diff --git a/changelog.md b/changelog.md index 7b679fb678..c45bf10825 100644 --- a/changelog.md +++ b/changelog.md @@ -148,7 +148,7 @@ * [1848](https://github.com/zeta-chain/node/issues/1848) - create a method to observe deposits to tss address in one evm block * [1885](https://github.com/zeta-chain/node/pull/1885) - change important metrics on port 8123 to be prometheus compatible * [1863](https://github.com/zeta-chain/node/pull/1863) - remove duplicate ValidateChainParams function -* [1914](https://github.com/zeta-chain/node/pull/1914) - move crosschain flags to core context in zetaclient +* [1914](https://github.com/zeta-chain/node/pull/1914) - move crosschain flags to app context in zetaclient * [1948](https://github.com/zeta-chain/node/pull/1948) - remove deprecated GetTSSAddress query in crosschain module * [1936](https://github.com/zeta-chain/node/pull/1936) - refactor common package into subpackages and rename to pkg * [1966](https://github.com/zeta-chain/node/pull/1966) - move TSS vote message from crosschain to observer @@ -177,6 +177,7 @@ * [2046](https://github.com/zeta-chain/node/pull/2046) - add state variable in crosschain for rate limiter flags * [2034](https://github.com/zeta-chain/node/pull/2034) - add support for zEVM message passing * [1825](https://github.com/zeta-chain/node/pull/1825) - add a message to withdraw emission rewards +* [2411](https://github.com/zeta-chain/node/pull/2411) - zetaclient activates and deactivates chains at runtime ### Tests diff --git a/cmd/zetaclientd/debug.go b/cmd/zetaclientd/debug.go index 28a3932a8d..c6f05d4bf9 100644 --- a/cmd/zetaclientd/debug.go +++ b/cmd/zetaclientd/debug.go @@ -11,7 +11,6 @@ import ( ethcommon "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/ethclient" "github.com/onrik/ethrpc" - "github.com/rs/zerolog" "github.com/spf13/cobra" "github.com/zeta-chain/zetacore/pkg/chains" @@ -57,7 +56,7 @@ func debugCmd(_ *cobra.Command, args []string) error { return err } - appContext := clientcontext.New(cfg, zerolog.Nop()) + appContext := clientcontext.New(cfg) chainID, err := strconv.ParseInt(args[1], 10, 64) if err != nil { @@ -131,7 +130,7 @@ func debugCmd(_ *cobra.Command, args []string) error { ZetaTokenContractAddress: chainParams.ZetaTokenContractAddress, Erc20CustodyContractAddress: chainParams.Erc20CustodyContractAddress, }) - evmChainParams, found := appContext.GetEVMChainParams(chainID) + evmChainParams, found := appContext.GetExternalChainParams(chainID) if !found { return fmt.Errorf("missing chain params for chain %d", chainID) } diff --git a/cmd/zetaclientd/keygen_tss.go b/cmd/zetaclientd/keygen_tss.go index 63b5d98041..ee66920b39 100644 --- a/cmd/zetaclientd/keygen_tss.go +++ b/cmd/zetaclientd/keygen_tss.go @@ -22,7 +22,8 @@ import ( "github.com/zeta-chain/zetacore/zetaclient/zetacore" ) -func GenerateTss( +// GenerateTSS waits for the keygen block height to arrive and generates a new TSS +func GenerateTSS( appContext *context.AppContext, logger zerolog.Logger, client *zetacore.Client, @@ -31,18 +32,10 @@ func GenerateTss( ts *metrics.TelemetryServer, tssHistoricalList []observertypes.TSS, tssPassword string, - hotkeyPassword string) (*mc.TSS, error) { + hotkeyPassword string, +) (*mc.TSS, error) { 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() - if btcEnabled { - bitcoinChainID = btcChain.ChainId - } - tss, err := mc.NewTSS( appContext, peers, @@ -50,7 +43,6 @@ func GenerateTss( preParams, client, tssHistoricalList, - bitcoinChainID, tssPassword, hotkeyPassword, ) @@ -74,7 +66,11 @@ 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, err := client.GetKeyGen() + if err != nil { + keygenLogger.Error().Err(err).Msg("GetKeyGen error") + continue + } if keyGen.Status == observertypes.KeygenStatus_KeyGenSuccess { return tss, nil } @@ -107,7 +103,7 @@ func GenerateTss( } // 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(*keyGen, tss, keygenLogger) if err != nil { keygenLogger.Error().Err(err).Msg("keygenTss error") tssFailedVoteHash, err := client.SetTSS("", keyGen.BlockNumber, chains.ReceiveStatus_failed) diff --git a/cmd/zetaclientd/start.go b/cmd/zetaclientd/start.go index 3546cec8ad..be26719441 100644 --- a/cmd/zetaclientd/start.go +++ b/cmd/zetaclientd/start.go @@ -6,11 +6,8 @@ import ( "fmt" "io" "os" - "os/signal" "path/filepath" "strings" - "syscall" - "time" "github.com/cometbft/cometbft/crypto/secp256k1" ethcommon "github.com/ethereum/go-ethereum/common" @@ -23,16 +20,23 @@ import ( "github.com/zeta-chain/zetacore/pkg/authz" "github.com/zeta-chain/zetacore/pkg/constant" - observerTypes "github.com/zeta-chain/zetacore/x/observer/types" + authzclient "github.com/zeta-chain/zetacore/zetaclient/authz" "github.com/zeta-chain/zetacore/zetaclient/chains/base" + "github.com/zeta-chain/zetacore/zetaclient/compliance" "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/orchestrator" + "github.com/zeta-chain/zetacore/zetaclient/zetacore" ) type Multiaddr = core.Multiaddr +const ( + // ObserverDBPath is the path (relative to user's home) to the observer database. + ObserverDBPath = ".zetaclient/chainobserver" +) + var StartCmd = &cobra.Command{ Use: "start", Short: "Start ZetaClient Observer", @@ -57,18 +61,11 @@ func start(_ *cobra.Command, _ []string) error { return err } - //Load Config file given path + // Load Config file from given path cfg, err := config.Load(rootArgs.zetaCoreHome) if err != nil { return err } - logger, err := base.InitLogger(cfg) - if err != nil { - log.Error().Err(err).Msg("InitLogger failed") - return err - } - - //Wait until zetacore has started if len(cfg.Peer) != 0 { err := validatePeer(cfg.Peer) if err != nil { @@ -77,14 +74,26 @@ func start(_ *cobra.Command, _ []string) error { } } + // Load compliance config + compliance.LoadComplianceConfig(cfg) + + // Initialize base logger + logger, err := base.InitLogger(cfg) + if err != nil { + log.Error().Err(err).Msg("InitLogger failed") + return err + } masterLogger := logger.Std startLogger := masterLogger.With().Str("module", "startup").Logger() + startLogger.Info().Msgf("zetaclient config file: \n%s", maskCfg(cfg)) // Wait until zetacore is up waitForZetaCore(cfg, startLogger) - startLogger.Info().Msgf("Zetacore is ready, trying to connect to %s", cfg.Peer) + startLogger.Info().Msgf("zetacore is ready, trying to connect to %s", cfg.Peer) + // Start telemetry server telemetryServer := metrics.NewTelemetryServer() + telemetryServer.SetIPAddress(cfg.PublicIP) go func() { err := telemetryServer.Start() if err != nil { @@ -93,11 +102,21 @@ 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) + // Start metrics server + m, err := metrics.NewMetrics() + if err != nil { + log.Error().Err(err).Msg("NewMetrics") + return err + } + m.Start() + metrics.Info.WithLabelValues(constant.Version).Set(1) + metrics.LastStartTime.SetToCurrentTime() + + // Create zetacore client to communicate with zetacore. + // Zetacore accumulates votes, and provides a centralized source of truth for all clients + zetacoreClient, err := zetacore.CreateClient(cfg, telemetryServer, hotkeyPass) if err != nil { - startLogger.Error().Err(err).Msg("CreateZetacoreClient error") + startLogger.Error().Err(err).Msg("Create zetacore client error") return err } @@ -133,26 +152,24 @@ func start(_ *cobra.Command, _ []string) error { } } - // CreateAuthzSigner : which is used to sign all authz messages . All votes broadcast to zetacore are wrapped in authz exec . - // This is to ensure that the user does not need to keep their operator key online , and can use a cold key to sign votes - signerAddress, err := zetacoreClient.GetKeys().GetAddress() + // Set up authz signer to sign all authz messages. All votes broadcast to zetacore are wrapped in authz exec. + // This is to ensure that the user does not need to keep their operator key online, and can use a cold key to sign votes + granter := zetacoreClient.GetKeys().GetOperatorAddress().String() + grantee, err := zetacoreClient.GetKeys().GetAddress() if err != nil { startLogger.Error().Err(err).Msg("error getting signer address") return err } - CreateAuthzSigner(zetacoreClient.GetKeys().GetOperatorAddress().String(), signerAddress) - startLogger.Debug().Msgf("CreateAuthzSigner is ready") + authzclient.SetupAuthZSignerList(granter, grantee) + startLogger.Info().Msgf("Authz is ready for granter %s grantee %s", granter, grantee) - // Initialize core parameters from zetacore - appContext := context.New(cfg, masterLogger) - err = zetacoreClient.UpdateZetacoreContext(appContext, true, startLogger) + // Initialize zetaclient app context + appContext := context.New(cfg) + err = zetacoreClient.UpdateAppContext(appContext, startLogger) if err != nil { - startLogger.Error().Err(err).Msg("Error getting core parameters") + startLogger.Error().Err(err).Msg("error initializing app context") return err } - startLogger.Info().Msgf("Config is updated from zetacore %s", maskCfg(cfg)) - - go zetacoreClient.ZetacoreContextUpdater(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 @@ -184,24 +201,13 @@ func start(_ *cobra.Command, _ []string) error { } } - m, err := metrics.NewMetrics() - if err != nil { - log.Error().Err(err).Msg("NewMetrics") - return err - } - m.Start() - - metrics.Info.WithLabelValues(constant.Version).Set(1) - metrics.LastStartTime.SetToCurrentTime() - - var tssHistoricalList []observerTypes.TSS - tssHistoricalList, err = zetacoreClient.GetTssHistory() + // Generate a new TSS key if there is a planned keygen ceremony + tssHistoricalList, err := zetacoreClient.GetTssHistory() if err != nil { startLogger.Error().Err(err).Msg("GetTssHistory error") } - telemetryServer.SetIPAddress(cfg.PublicIP) - tss, err := GenerateTss( + tss, err := GenerateTSS( appContext, masterLogger, zetacoreClient, @@ -222,18 +228,6 @@ func start(_ *cobra.Command, _ []string) error { } } - // Wait for TSS keygen to be successful before proceeding, This is a blocking thread only for a new keygen. - // For existing keygen, this should directly proceed to the next step - ticker := time.NewTicker(time.Second * 1) - for range ticker.C { - keyGen := appContext.GetKeygen() - if keyGen.Status != observerTypes.KeygenStatus_KeyGenSuccess { - startLogger.Info().Msgf("Waiting for TSS Keygen to be a success, current status %s", keyGen.Status) - continue - } - break - } - // 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 @@ -245,15 +239,19 @@ func start(_ *cobra.Command, _ []string) error { // Defensive check: Make sure the tss address is set to the current TSS address and not the newly generated one tss.CurrentPubkey = currentTss.TssPubkey - if tss.EVMAddress() == (ethcommon.Address{}) || tss.BTCAddress() == "" { - startLogger.Error().Msg("TSS address is not set in zetacore") - } - startLogger.Info(). - Msgf("Current TSS address \n ETH : %s \n BTC : %s \n PubKey : %s ", tss.EVMAddress(), tss.BTCAddress(), tss.CurrentPubkey) - if len(appContext.GetEnabledChains()) == 0 { - startLogger.Error().Msgf("No chains enabled in updated config %s ", cfg.String()) + startLogger.Info().Msgf("Current TSS PubKey: %s", tss.CurrentPubkey) + if tss.EVMAddress() == (ethcommon.Address{}) { + return errors.New("Current TSS ETH address is empty") + } + startLogger.Info().Msgf("Current TSS ETH address: %s", tss.EVMAddress()) + if tss.BitcoinNetParams != nil { + if tss.BTCAddress() == "" { + return errors.New("Current TSS BTC address is empty") + } + startLogger.Info().Msgf("Current TSS BTC address: %s", tss.BTCAddress()) } + // Stop zetaclient if this node is not an active observer observerList, err := zetacoreClient.GetObserverList() if err != nil { startLogger.Error().Err(err).Msg("GetObserverList error") @@ -261,50 +259,24 @@ func start(_ *cobra.Command, _ []string) error { } isNodeActive := false for _, observer := range observerList { - if observer == zetacoreClient.GetKeys().GetOperatorAddress().String() { + if observer == granter { isNodeActive = true break } } - - // 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) - if err != nil { - log.Error().Err(err).Msg("CreateSignerMap") - return err + if !isNodeActive { + startLogger.Error().Msgf("Node %s is not an active observer, zetaclient stopped", granter) + return nil } + startLogger.Info().Msgf("Node %s is an active observer, starting orchestrator", granter) + // use the user's home path to store observer database userDir, err := os.UserHomeDir() if err != nil { log.Error().Err(err).Msg("os.UserHomeDir") return err } - 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) - if err != nil { - startLogger.Err(err).Msg("CreateChainObserverMap") - return err - } - - if !isNodeActive { - startLogger.Error(). - Msgf("Node %s is not an active observer external chain observers will not be started", zetacoreClient.GetKeys().GetOperatorAddress().String()) - } else { - startLogger.Debug().Msgf("Node %s is an active observer starting external chain observers", zetacoreClient.GetKeys().GetOperatorAddress().String()) - for _, observer := range observerMap { - observer.Start() - } - } - - // 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) - if err != nil { - startLogger.Error().Err(err).Msg("Orchestrator failed to start") - return err - } + dbPath := filepath.Join(userDir, ObserverDBPath) // start zeta supply checker // TODO: enable @@ -320,17 +292,17 @@ func start(_ *cobra.Command, _ []string) error { // defer zetaSupplyChecker.Stop() //} - startLogger.Info().Msgf("awaiting the os.Interrupt, syscall.SIGTERM signals...") - ch := make(chan os.Signal, 1) - signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM) - sig := <-ch - startLogger.Info().Msgf("stop signal received: %s", sig) - - // stop chain observers - for _, observer := range observerMap { - observer.Stop() - } - zetacoreClient.Stop() + // Orchestrator wraps the app context, zetacore client, TSS and metrics server. + // This is the high level actor that monitors zetacore changes and coordinates CCTXs interactions. + orch := orchestrator.NewOrchestrator( + appContext, + zetacoreClient, + tss, + logger, + dbPath, + telemetryServer, + ) + orch.Start() return nil } diff --git a/cmd/zetaclientd/utils.go b/cmd/zetaclientd/utils.go deleted file mode 100644 index 99f6e03e59..0000000000 --- a/cmd/zetaclientd/utils.go +++ /dev/null @@ -1,194 +0,0 @@ -package main - -import ( - "fmt" - - sdk "github.com/cosmos/cosmos-sdk/types" - ethcommon "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/ethclient" - - "github.com/zeta-chain/zetacore/zetaclient/authz" - "github.com/zeta-chain/zetacore/zetaclient/chains/base" - btcobserver "github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin/observer" - btcrpc "github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin/rpc" - btcsigner "github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin/signer" - evmobserver "github.com/zeta-chain/zetacore/zetaclient/chains/evm/observer" - evmsigner "github.com/zeta-chain/zetacore/zetaclient/chains/evm/signer" - "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/zetacore" -) - -func CreateAuthzSigner(granter string, grantee sdk.AccAddress) { - authz.SetupAuthZSignerList(granter, grantee) -} - -func CreateZetacoreClient( - cfg config.Config, - telemetry *metrics.TelemetryServer, - hotkeyPassword string, -) (*zetacore.Client, error) { - hotKey := cfg.AuthzHotkey - if cfg.HsmMode { - hotKey = cfg.HsmHotKey - } - - chainIP := cfg.ZetaCoreURL - - kb, _, err := keys.GetKeyringKeybase(cfg, hotkeyPassword) - if err != nil { - return nil, err - } - - granterAddreess, err := sdk.AccAddressFromBech32(cfg.AuthzGranter) - if err != nil { - return nil, err - } - - k := keys.NewKeysWithKeybase(kb, granterAddreess, cfg.AuthzHotkey, hotkeyPassword) - - client, err := zetacore.NewClient(k, chainIP, hotKey, cfg.ChainID, cfg.HsmMode, telemetry) - if err != nil { - return nil, err - } - - return client, nil -} - -// CreateSignerMap creates a map of ChainSigners for all chains in the config -func CreateSignerMap( - appContext *context.AppContext, - tss interfaces.TSSSigner, - logger base.Logger, - ts *metrics.TelemetryServer, -) (map[int64]interfaces.ChainSigner, error) { - signerMap := make(map[int64]interfaces.ChainSigner) - - // EVM signers - for _, evmConfig := range appContext.Config().GetAllEVMConfigs() { - if evmConfig.Chain.IsZetaChain() { - continue - } - evmChainParams, found := appContext.GetEVMChainParams(evmConfig.Chain.ChainId) - if !found { - logger.Std.Error().Msgf("ChainParam not found for chain %s", evmConfig.Chain.String()) - continue - } - mpiAddress := ethcommon.HexToAddress(evmChainParams.ConnectorContractAddress) - erc20CustodyAddress := ethcommon.HexToAddress(evmChainParams.Erc20CustodyContractAddress) - signer, err := evmsigner.NewSigner( - evmConfig.Chain, - appContext, - tss, - ts, - logger, - evmConfig.Endpoint, - config.GetConnectorABI(), - config.GetERC20CustodyABI(), - mpiAddress, - erc20CustodyAddress) - if err != nil { - logger.Std.Error().Err(err).Msgf("NewEVMSigner error for chain %s", evmConfig.Chain.String()) - continue - } - signerMap[evmConfig.Chain.ChainId] = signer - } - // BTC signer - btcChain, btcConfig, enabled := appContext.GetBTCChainAndConfig() - if enabled { - signer, err := btcsigner.NewSigner(btcChain, appContext, tss, ts, logger, btcConfig) - if err != nil { - logger.Std.Error().Err(err).Msgf("NewBTCSigner error for chain %s", btcChain.String()) - } else { - signerMap[btcChain.ChainId] = signer - } - } - - return signerMap, nil -} - -// CreateChainObserverMap creates a map of ChainObservers for all chains in the config -func CreateChainObserverMap( - appContext *context.AppContext, - zetacoreClient *zetacore.Client, - tss interfaces.TSSSigner, - dbpath string, - logger base.Logger, - ts *metrics.TelemetryServer, -) (map[int64]interfaces.ChainObserver, error) { - observerMap := make(map[int64]interfaces.ChainObserver) - // EVM observers - for _, evmConfig := range appContext.Config().GetAllEVMConfigs() { - if evmConfig.Chain.IsZetaChain() { - continue - } - chainParams, found := appContext.GetEVMChainParams(evmConfig.Chain.ChainId) - if !found { - logger.Std.Error().Msgf("ChainParam not found for chain %s", evmConfig.Chain.String()) - continue - } - - // create EVM client - evmClient, err := ethclient.Dial(evmConfig.Endpoint) - if err != nil { - logger.Std.Error().Err(err).Msgf("error dailing endpoint %s", evmConfig.Endpoint) - continue - } - - // create EVM chain observer - observer, err := evmobserver.NewObserver( - evmConfig, - evmClient, - *chainParams, - appContext, - zetacoreClient, - tss, - dbpath, - logger, - ts, - ) - if err != nil { - logger.Std.Error().Err(err).Msgf("NewObserver error for evm chain %s", evmConfig.Chain.String()) - continue - } - observerMap[evmConfig.Chain.ChainId] = observer - } - - // BTC observer - _, chainParams, found := appContext.GetBTCChainParams() - if !found { - return nil, fmt.Errorf("bitcoin chains params not found") - } - - // create BTC chain observer - btcChain, btcConfig, enabled := appContext.GetBTCChainAndConfig() - if enabled { - btcClient, err := btcrpc.NewRPCClient(btcConfig) - if err != nil { - logger.Std.Error().Err(err).Msgf("error creating rpc client for bitcoin chain %s", btcChain.String()) - } else { - // create BTC chain observer - observer, err := btcobserver.NewObserver( - btcChain, - btcClient, - *chainParams, - appContext, - zetacoreClient, - tss, - dbpath, - logger, - ts, - ) - if err != nil { - logger.Std.Error().Err(err).Msgf("NewObserver error for bitcoin chain %s", btcChain.String()) - } else { - observerMap[btcChain.ChainId] = observer - } - } - } - - return observerMap, nil -} diff --git a/zetaclient/chains/base/observer.go b/zetaclient/chains/base/observer.go index edfef83629..d0b71f4a3c 100644 --- a/zetaclient/chains/base/observer.go +++ b/zetaclient/chains/base/observer.go @@ -129,7 +129,7 @@ func NewObserver( // Stop notifies all goroutines to stop and closes the database. func (ob *Observer) Stop() { - ob.logger.Chain.Info().Msgf("observer is stopping for chain %d", ob.Chain().ChainId) + ob.logger.Chain.Info().Msgf("Observer is stopping for chain %d", ob.Chain().ChainId) close(ob.stop) // close database @@ -138,8 +138,9 @@ func (ob *Observer) Stop() { if err != nil { ob.Logger().Chain.Error().Err(err).Msgf("CloseDB failed for chain %d", ob.Chain().ChainId) } + ob.db = nil } - ob.Logger().Chain.Info().Msgf("observer stopped for chain %d", ob.Chain().ChainId) + ob.Logger().Chain.Info().Msgf("Observer stopped for chain %d", ob.Chain().ChainId) } // Chain returns the chain for the observer. @@ -164,7 +165,7 @@ func (ob *Observer) WithChainParams(params observertypes.ChainParams) *Observer return ob } -// AppContext returns the zetacore context for the observer. +// AppContext returns the app context for the observer. func (ob *Observer) AppContext() *context.AppContext { return ob.appContext } diff --git a/zetaclient/chains/base/observer_test.go b/zetaclient/chains/base/observer_test.go index e6d5a088a9..54513acce4 100644 --- a/zetaclient/chains/base/observer_test.go +++ b/zetaclient/chains/base/observer_test.go @@ -5,7 +5,6 @@ import ( "testing" lru "github.com/hashicorp/golang-lru" - "github.com/rs/zerolog" "github.com/rs/zerolog/log" "github.com/stretchr/testify/require" "github.com/zeta-chain/zetacore/zetaclient/testutils" @@ -26,7 +25,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()) + appContext := context.New(config.NewConfig()) zetacoreClient := mocks.NewMockZetacoreClient() tss := mocks.NewTSSMainnet() @@ -52,7 +51,7 @@ func TestNewObserver(t *testing.T) { // constructor parameters chain := chains.Ethereum chainParams := *sample.ChainParams(chain.ChainId) - appContext := context.New(config.NewConfig(), zerolog.Nop()) + appContext := context.New(config.NewConfig()) zetacoreClient := mocks.NewMockZetacoreClient() tss := mocks.NewTSSMainnet() blockCacheSize := base.DefaultBlockCacheSize @@ -142,6 +141,9 @@ func TestStop(t *testing.T) { // stop observer ob.Stop() + + // db should be removed + require.Nil(t, ob.DB()) }) } diff --git a/zetaclient/chains/base/signer.go b/zetaclient/chains/base/signer.go index 2ac38e048d..21dde4a746 100644 --- a/zetaclient/chains/base/signer.go +++ b/zetaclient/chains/base/signer.go @@ -15,6 +15,7 @@ type Signer struct { // chain contains static information about the external chain chain chains.Chain + // appContext contains context data of zetaclient appContext *context.AppContext // tss is the TSS signer @@ -62,7 +63,7 @@ func (s *Signer) WithChain(chain chains.Chain) *Signer { return s } -// AppContext returns the zetacore context for the signer +// AppContext returns the app context for the signer func (s *Signer) AppContext() *context.AppContext { return s.appContext } diff --git a/zetaclient/chains/base/signer_test.go b/zetaclient/chains/base/signer_test.go index a0e2696b92..5235b21683 100644 --- a/zetaclient/chains/base/signer_test.go +++ b/zetaclient/chains/base/signer_test.go @@ -3,7 +3,6 @@ package base_test import ( "testing" - "github.com/rs/zerolog" "github.com/stretchr/testify/require" "github.com/zeta-chain/zetacore/pkg/chains" @@ -18,7 +17,7 @@ import ( func createSigner(_ *testing.T) *base.Signer { // constructor parameters chain := chains.Ethereum - appContext := context.New(config.NewConfig(), zerolog.Nop()) + appContext := context.New(config.NewConfig()) tss := mocks.NewTSSMainnet() logger := base.DefaultLogger() diff --git a/zetaclient/chains/bitcoin/observer/inbound.go b/zetaclient/chains/bitcoin/observer/inbound.go index 1d528a7c62..26079f4303 100644 --- a/zetaclient/chains/bitcoin/observer/inbound.go +++ b/zetaclient/chains/bitcoin/observer/inbound.go @@ -19,7 +19,6 @@ import ( "github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin" "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/types" "github.com/zeta-chain/zetacore/zetaclient/zetacore" ) @@ -373,7 +372,7 @@ func (ob *Observer) DoesInboundContainsRestrictedAddress(inTx *BTCInboundEvent) if err == nil && parsedAddress != (ethcommon.Address{}) { receiver = parsedAddress.Hex() } - if config.ContainRestrictedAddress(inTx.FromAddress, receiver) { + if compliance.ContainRestrictedAddress(inTx.FromAddress, receiver) { compliance.PrintComplianceLog(ob.logger.Inbound, ob.logger.Compliance, false, ob.Chain().ChainId, inTx.TxHash, inTx.FromAddress, receiver, "BTC") return true diff --git a/zetaclient/chains/bitcoin/observer/observer.go b/zetaclient/chains/bitcoin/observer/observer.go index a04188da80..d40a4701ef 100644 --- a/zetaclient/chains/bitcoin/observer/observer.go +++ b/zetaclient/chains/bitcoin/observer/observer.go @@ -115,7 +115,6 @@ func NewObserver( appContext *context.AppContext, zetacoreClient interfaces.ZetacoreClient, tss interfaces.TSSSigner, - dbpath string, logger base.Logger, ts *metrics.TelemetryServer, ) (*Observer, error) { @@ -157,12 +156,6 @@ func NewObserver( }, } - // load btc chain observer DB - err = ob.LoadDB(dbpath) - if err != nil { - return nil, err - } - return ob, nil } @@ -219,7 +212,7 @@ func (ob *Observer) Start() { // TODO(revamp): move ticker related functions to a specific file // TODO(revamp): move inner logic in a separate function func (ob *Observer) WatchRPCStatus() { - ob.logger.Chain.Info().Msgf("RPCStatus is starting") + ob.logger.Chain.Info().Msgf("WatchRPCStatus started for chain %d", ob.Chain().ChainId) ticker := time.NewTicker(60 * time.Second) for { @@ -274,6 +267,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(): + ob.Logger().Chain.Info().Msgf("WatchRPCStatus stopped for chain %d", ob.Chain().ChainId) return } } @@ -436,8 +430,9 @@ func (ob *Observer) WatchUTXOs() { ob.logger.UTXOs.Error().Err(err).Msg("error creating ticker") return } - defer ticker.Stop() + ob.logger.Outbound.Info().Msgf("WatchUTXOs started for chain %d", ob.Chain().ChainId) + for { select { case <-ticker.C(): diff --git a/zetaclient/chains/bitcoin/observer/observer_test.go b/zetaclient/chains/bitcoin/observer/observer_test.go index 8fb8838ae3..e5433f796a 100644 --- a/zetaclient/chains/bitcoin/observer/observer_test.go +++ b/zetaclient/chains/bitcoin/observer/observer_test.go @@ -93,12 +93,15 @@ func MockBTCObserver( nil, nil, nil, - dbpath, base.Logger{}, nil, ) require.NoError(t, err) + // load db + err = ob.LoadDB(dbpath) + require.NoError(t, err) + return ob } @@ -116,7 +119,6 @@ func Test_NewObserver(t *testing.T) { appContext *context.AppContext coreClient interfaces.ZetacoreClient tss interfaces.TSSSigner - dbpath string logger base.Logger ts *metrics.TelemetryServer fail bool @@ -130,7 +132,6 @@ func Test_NewObserver(t *testing.T) { appContext: nil, coreClient: nil, tss: mocks.NewTSSMainnet(), - dbpath: sample.CreateTempDir(t), logger: base.Logger{}, ts: nil, fail: false, @@ -143,26 +144,11 @@ func Test_NewObserver(t *testing.T) { appContext: nil, coreClient: nil, tss: mocks.NewTSSMainnet(), - dbpath: sample.CreateTempDir(t), logger: base.Logger{}, ts: nil, fail: true, message: "error getting net params", }, - { - name: "should fail on invalid dbpath", - chain: chain, - chainParams: params, - appContext: nil, - coreClient: nil, - btcClient: mocks.NewMockBTCRPCClient().WithBlockCount(100), - tss: mocks.NewTSSMainnet(), - dbpath: "/invalid/dbpath", // invalid dbpath - logger: base.Logger{}, - ts: nil, - fail: true, - message: "error creating db path", - }, } // run tests @@ -176,7 +162,6 @@ func Test_NewObserver(t *testing.T) { tt.appContext, tt.coreClient, tt.tss, - tt.dbpath, tt.logger, tt.ts, ) @@ -254,7 +239,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, nil, tss, 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_test.go b/zetaclient/chains/bitcoin/observer/outbound_test.go index 0aaeb7b600..588f6081fd 100644 --- a/zetaclient/chains/bitcoin/observer/outbound_test.go +++ b/zetaclient/chains/bitcoin/observer/outbound_test.go @@ -27,7 +27,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, nil, tss, base.Logger{}, nil) require.NoError(t, err) return ob diff --git a/zetaclient/chains/bitcoin/rpc/rpc_live_test.go b/zetaclient/chains/bitcoin/rpc/rpc_live_test.go index 97a373f94d..618d5f89f2 100644 --- a/zetaclient/chains/bitcoin/rpc/rpc_live_test.go +++ b/zetaclient/chains/bitcoin/rpc/rpc_live_test.go @@ -55,8 +55,7 @@ func (suite *BitcoinObserverTestSuite) SetupTest() { btcClient := mocks.NewMockBTCRPCClient() // create observer - ob, err := observer.NewObserver(chain, btcClient, params, nil, nil, tss, testutils.SQLiteMemory, - base.DefaultLogger(), nil) + ob, err := observer.NewObserver(chain, btcClient, params, nil, nil, tss, base.DefaultLogger(), nil) suite.Require().NoError(err) suite.Require().NotNil(ob) suite.rpcClient, err = getRPCClient(18332) diff --git a/zetaclient/chains/evm/constant.go b/zetaclient/chains/evm/constant.go index b754d57f30..a00db1b538 100644 --- a/zetaclient/chains/evm/constant.go +++ b/zetaclient/chains/evm/constant.go @@ -3,9 +3,6 @@ package evm import "time" const ( - // ZetaBlockTime is the block time of the Zeta network - ZetaBlockTime = 6500 * time.Millisecond - // OutboundInclusionTimeout is the timeout for waiting for an outbound to be included in a block OutboundInclusionTimeout = 20 * time.Minute diff --git a/zetaclient/chains/evm/observer/inbound.go b/zetaclient/chains/evm/observer/inbound.go index 889d2215ac..180b6f161b 100644 --- a/zetaclient/chains/evm/observer/inbound.go +++ b/zetaclient/chains/evm/observer/inbound.go @@ -82,7 +82,7 @@ func (ob *Observer) WatchInboundTracker() { } defer ticker.Stop() - ob.Logger().Inbound.Info().Msgf("Inbound tracker watcher started for chain %d", ob.Chain().ChainId) + ob.Logger().Inbound.Info().Msgf("WatchInboundTracker started for chain %d", ob.Chain().ChainId) for { select { case <-ticker.C(): @@ -594,7 +594,7 @@ func (ob *Observer) BuildInboundVoteMsgForDepositedEvent( if err == nil && parsedAddress != (ethcommon.Address{}) { maybeReceiver = parsedAddress.Hex() } - if config.ContainRestrictedAddress(sender.Hex(), clienttypes.BytesToEthHex(event.Recipient), maybeReceiver) { + if compliance.ContainRestrictedAddress(sender.Hex(), clienttypes.BytesToEthHex(event.Recipient), maybeReceiver) { compliance.PrintComplianceLog( ob.Logger().Inbound, ob.Logger().Compliance, @@ -654,17 +654,17 @@ func (ob *Observer) BuildInboundVoteMsgForZetaSentEvent( // compliance check sender := event.ZetaTxSenderAddress.Hex() - if config.ContainRestrictedAddress(sender, destAddr, event.SourceTxOriginAddress.Hex()) { + if compliance.ContainRestrictedAddress(sender, destAddr, event.SourceTxOriginAddress.Hex()) { compliance.PrintComplianceLog(ob.Logger().Inbound, ob.Logger().Compliance, false, ob.Chain().ChainId, event.Raw.TxHash.Hex(), sender, destAddr, "Zeta") return nil } if !destChain.IsZetaChain() { - paramsDest, found := ob.AppContext().GetEVMChainParams(destChain.ChainId) + paramsDest, found := ob.AppContext().GetExternalChainParams(destChain.ChainId) if !found { ob.Logger().Inbound.Warn(). - Msgf("chain id not present in EVMChainParams %d", event.DestinationChainId.Int64()) + Msgf("chain params id not present in AppContext %d", destChain.ChainId) return nil } @@ -711,7 +711,7 @@ func (ob *Observer) BuildInboundVoteMsgForTokenSentToTSS( if err == nil && parsedAddress != (ethcommon.Address{}) { maybeReceiver = parsedAddress.Hex() } - if config.ContainRestrictedAddress(sender.Hex(), maybeReceiver) { + if compliance.ContainRestrictedAddress(sender.Hex(), maybeReceiver) { compliance.PrintComplianceLog(ob.Logger().Inbound, ob.Logger().Compliance, false, ob.Chain().ChainId, tx.Hash, sender.Hex(), sender.Hex(), "Gas") return nil diff --git a/zetaclient/chains/evm/observer/inbound_test.go b/zetaclient/chains/evm/observer/inbound_test.go index bb8930f4ca..469eeef524 100644 --- a/zetaclient/chains/evm/observer/inbound_test.go +++ b/zetaclient/chains/evm/observer/inbound_test.go @@ -13,6 +13,7 @@ import ( "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/compliance" "github.com/zeta-chain/zetacore/zetaclient/config" "github.com/zeta-chain/zetacore/zetaclient/keys" "github.com/zeta-chain/zetacore/zetaclient/testutils" @@ -40,7 +41,7 @@ func Test_CheckAndVoteInboundTokenZeta(t *testing.T) { require.NoError(t, evm.ValidateEvmTransaction(tx)) lastBlock := receipt.BlockNumber.Uint64() + confirmation - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, lastBlock, chainParam) + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, chainParam) ballot, err := ob.CheckAndVoteInboundTokenZeta(tx, receipt, false) require.NoError(t, err) require.Equal(t, cctx.InboundParams.BallotIndex, ballot) @@ -56,7 +57,7 @@ func Test_CheckAndVoteInboundTokenZeta(t *testing.T) { require.NoError(t, evm.ValidateEvmTransaction(tx)) lastBlock := receipt.BlockNumber.Uint64() + confirmation - 1 - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, lastBlock, chainParam) + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, chainParam) _, err := ob.CheckAndVoteInboundTokenZeta(tx, receipt, false) require.ErrorContains(t, err, "not been confirmed") }) @@ -72,7 +73,7 @@ func Test_CheckAndVoteInboundTokenZeta(t *testing.T) { require.NoError(t, evm.ValidateEvmTransaction(tx)) lastBlock := receipt.BlockNumber.Uint64() + confirmation - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, lastBlock, chainParam) + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, chainParam) ballot, err := ob.CheckAndVoteInboundTokenZeta(tx, receipt, true) require.NoError(t, err) require.Equal(t, "", ballot) @@ -96,7 +97,6 @@ func Test_CheckAndVoteInboundTokenZeta(t *testing.T) { nil, nil, nil, - memDBPath, lastBlock, mocks.MockChainParams(chainID, confirmation), ) @@ -125,7 +125,7 @@ func Test_CheckAndVoteInboundTokenERC20(t *testing.T) { require.NoError(t, evm.ValidateEvmTransaction(tx)) lastBlock := receipt.BlockNumber.Uint64() + confirmation - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, lastBlock, chainParam) + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, chainParam) ballot, err := ob.CheckAndVoteInboundTokenERC20(tx, receipt, false) require.NoError(t, err) require.Equal(t, cctx.InboundParams.BallotIndex, ballot) @@ -141,7 +141,7 @@ func Test_CheckAndVoteInboundTokenERC20(t *testing.T) { require.NoError(t, evm.ValidateEvmTransaction(tx)) lastBlock := receipt.BlockNumber.Uint64() + confirmation - 1 - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, lastBlock, chainParam) + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, chainParam) _, err := ob.CheckAndVoteInboundTokenERC20(tx, receipt, false) require.ErrorContains(t, err, "not been confirmed") }) @@ -157,7 +157,7 @@ func Test_CheckAndVoteInboundTokenERC20(t *testing.T) { require.NoError(t, evm.ValidateEvmTransaction(tx)) lastBlock := receipt.BlockNumber.Uint64() + confirmation - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, lastBlock, chainParam) + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, chainParam) ballot, err := ob.CheckAndVoteInboundTokenERC20(tx, receipt, true) require.NoError(t, err) require.Equal(t, "", ballot) @@ -181,7 +181,6 @@ func Test_CheckAndVoteInboundTokenERC20(t *testing.T) { nil, nil, nil, - memDBPath, lastBlock, mocks.MockChainParams(chainID, confirmation), ) @@ -210,7 +209,7 @@ func Test_CheckAndVoteInboundTokenGas(t *testing.T) { require.NoError(t, evm.ValidateEvmTransaction(tx)) lastBlock := receipt.BlockNumber.Uint64() + confirmation - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, lastBlock, chainParam) + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, chainParam) ballot, err := ob.CheckAndVoteInboundTokenGas(tx, receipt, false) require.NoError(t, err) require.Equal(t, cctx.InboundParams.BallotIndex, ballot) @@ -220,7 +219,7 @@ func Test_CheckAndVoteInboundTokenGas(t *testing.T) { require.NoError(t, evm.ValidateEvmTransaction(tx)) lastBlock := receipt.BlockNumber.Uint64() + confirmation - 1 - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, lastBlock, chainParam) + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, chainParam) _, err := ob.CheckAndVoteInboundTokenGas(tx, receipt, false) require.ErrorContains(t, err, "not been confirmed") }) @@ -230,7 +229,7 @@ func Test_CheckAndVoteInboundTokenGas(t *testing.T) { require.NoError(t, evm.ValidateEvmTransaction(tx)) lastBlock := receipt.BlockNumber.Uint64() + confirmation - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, lastBlock, chainParam) + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, chainParam) ballot, err := ob.CheckAndVoteInboundTokenGas(tx, receipt, false) require.ErrorContains(t, err, "not TSS address") require.Equal(t, "", ballot) @@ -241,7 +240,7 @@ func Test_CheckAndVoteInboundTokenGas(t *testing.T) { require.NoError(t, evm.ValidateEvmTransaction(tx)) lastBlock := receipt.BlockNumber.Uint64() + confirmation - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, lastBlock, chainParam) + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, chainParam) ballot, err := ob.CheckAndVoteInboundTokenGas(tx, receipt, false) require.ErrorContains(t, err, "not a successful tx") require.Equal(t, "", ballot) @@ -252,7 +251,7 @@ func Test_CheckAndVoteInboundTokenGas(t *testing.T) { require.NoError(t, evm.ValidateEvmTransaction(tx)) lastBlock := receipt.BlockNumber.Uint64() + confirmation - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, lastBlock, chainParam) + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, chainParam) ballot, err := ob.CheckAndVoteInboundTokenGas(tx, receipt, false) require.NoError(t, err) require.Equal(t, "", ballot) @@ -269,7 +268,7 @@ func Test_BuildInboundVoteMsgForZetaSentEvent(t *testing.T) { cctx := testutils.LoadCctxByInbound(t, chainID, coin.CoinType_Zeta, inboundHash) // parse ZetaSent event - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, 1, mocks.MockChainParams(1, 1)) + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, 1, mocks.MockChainParams(1, 1)) connector := mocks.MockConnectorNonEth(t, chainID) event := testutils.ParseReceiptZetaSent(receipt, connector) @@ -286,21 +285,21 @@ func Test_BuildInboundVoteMsgForZetaSentEvent(t *testing.T) { t.Run("should return nil msg if sender is restricted", func(t *testing.T) { sender := event.ZetaTxSenderAddress.Hex() cfg.ComplianceConfig.RestrictedAddresses = []string{sender} - config.LoadComplianceConfig(cfg) + compliance.LoadComplianceConfig(cfg) msg := ob.BuildInboundVoteMsgForZetaSentEvent(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) + compliance.LoadComplianceConfig(cfg) msg := ob.BuildInboundVoteMsgForZetaSentEvent(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) + compliance.LoadComplianceConfig(cfg) msg := ob.BuildInboundVoteMsgForZetaSentEvent(event) require.Nil(t, msg) }) @@ -316,7 +315,7 @@ func Test_BuildInboundVoteMsgForDepositedEvent(t *testing.T) { cctx := testutils.LoadCctxByInbound(t, chainID, coin.CoinType_ERC20, inboundHash) // parse Deposited event - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, 1, mocks.MockChainParams(1, 1)) + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, 1, mocks.MockChainParams(1, 1)) custody := mocks.MockERC20Custody(t, chainID) event := testutils.ParseReceiptERC20Deposited(receipt, custody) sender := ethcommon.HexToAddress(tx.From) @@ -333,14 +332,14 @@ func Test_BuildInboundVoteMsgForDepositedEvent(t *testing.T) { }) t.Run("should return nil msg if sender is restricted", func(t *testing.T) { cfg.ComplianceConfig.RestrictedAddresses = []string{sender.Hex()} - config.LoadComplianceConfig(cfg) + compliance.LoadComplianceConfig(cfg) msg := ob.BuildInboundVoteMsgForDepositedEvent(event, sender) require.Nil(t, msg) }) t.Run("should return nil msg if receiver is restricted", func(t *testing.T) { receiver := clienttypes.BytesToEthHex(event.Recipient) cfg.ComplianceConfig.RestrictedAddresses = []string{receiver} - config.LoadComplianceConfig(cfg) + compliance.LoadComplianceConfig(cfg) msg := ob.BuildInboundVoteMsgForDepositedEvent(event, sender) require.Nil(t, msg) }) @@ -374,7 +373,7 @@ func Test_BuildInboundVoteMsgForTokenSentToTSS(t *testing.T) { require.NoError(t, evm.ValidateEvmTransaction(txDonation)) // create test compliance config - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, 1, mocks.MockChainParams(1, 1)) + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, 1, mocks.MockChainParams(1, 1)) cfg := config.Config{ ComplianceConfig: config.ComplianceConfig{}, } @@ -390,7 +389,7 @@ func Test_BuildInboundVoteMsgForTokenSentToTSS(t *testing.T) { }) t.Run("should return nil msg if sender is restricted", func(t *testing.T) { cfg.ComplianceConfig.RestrictedAddresses = []string{tx.From} - config.LoadComplianceConfig(cfg) + compliance.LoadComplianceConfig(cfg) msg := ob.BuildInboundVoteMsgForTokenSentToTSS( tx, ethcommon.HexToAddress(tx.From), @@ -404,7 +403,7 @@ func Test_BuildInboundVoteMsgForTokenSentToTSS(t *testing.T) { message := hex.EncodeToString(ethcommon.HexToAddress(testutils.OtherAddress1).Bytes()) txCopy.Input = message // use other address as receiver cfg.ComplianceConfig.RestrictedAddresses = []string{testutils.OtherAddress1} - config.LoadComplianceConfig(cfg) + compliance.LoadComplianceConfig(cfg) msg := ob.BuildInboundVoteMsgForTokenSentToTSS( txCopy, ethcommon.HexToAddress(txCopy.From), @@ -444,7 +443,7 @@ func Test_ObserveTSSReceiveInBlock(t *testing.T) { lastBlock := receipt.BlockNumber.Uint64() + confirmation t.Run("should observe TSS receive in block", func(t *testing.T) { - ob := MockEVMObserver(t, chain, evmClient, evmJSONRPC, zetacoreClient, tss, memDBPath, lastBlock, chainParam) + ob := MockEVMObserver(t, chain, evmClient, evmJSONRPC, zetacoreClient, tss, lastBlock, chainParam) // feed archived block and receipt evmJSONRPC.WithBlock(block) @@ -453,20 +452,20 @@ func Test_ObserveTSSReceiveInBlock(t *testing.T) { 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) + ob := MockEVMObserver(t, chain, evmClient, evmJSONRPC, zetacoreClient, tss, lastBlock, chainParam) err := ob.ObserveTSSReceiveInBlock(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) + ob := MockEVMObserver(t, chain, evmClient, evmJSONRPC, zetacoreClient, tss, lastBlock, chainParam) evmJSONRPC.WithBlock(block) err := ob.ObserveTSSReceiveInBlock(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) + ob := MockEVMObserver(t, chain, evmClient, evmJSONRPC, zetacoreClient, tss, lastBlock, chainParam) // feed archived block and pause zetacore client evmJSONRPC.WithBlock(block) diff --git a/zetaclient/chains/evm/observer/observer.go b/zetaclient/chains/evm/observer/observer.go index 3a60b16e40..a0d7c3118f 100644 --- a/zetaclient/chains/evm/observer/observer.go +++ b/zetaclient/chains/evm/observer/observer.go @@ -58,10 +58,9 @@ func NewObserver( evmCfg config.EVMConfig, evmClient interfaces.EVMRPCClient, chainParams observertypes.ChainParams, - appClient *clientcontext.AppContext, + appContext *clientcontext.AppContext, zetacoreClient interfaces.ZetacoreClient, tss interfaces.TSSSigner, - dbpath string, logger base.Logger, ts *metrics.TelemetryServer, ) (*Observer, error) { @@ -69,7 +68,7 @@ func NewObserver( baseObserver, err := base.NewObserver( evmCfg.Chain, chainParams, - appClient, + appContext, zetacoreClient, tss, base.DefaultBlockCacheSize, @@ -91,12 +90,6 @@ func NewObserver( outboundConfirmedTransactions: make(map[string]*ethtypes.Transaction), } - // open database and load data - err = ob.LoadDB(dbpath) - if err != nil { - return nil, err - } - return ob, nil } @@ -189,7 +182,7 @@ func (ob *Observer) Start() { // TODO(revamp): move ticker to ticker file // TODO(revamp): move inner logic to a separate function func (ob *Observer) WatchRPCStatus() { - ob.Logger().Chain.Info().Msgf("Starting RPC status check for chain %d", ob.Chain().ChainId) + ob.Logger().Chain.Info().Msgf("WatchRPCStatus started for chain %d", ob.Chain().ChainId) ticker := time.NewTicker(60 * time.Second) for { select { @@ -223,6 +216,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(): + ob.Logger().Chain.Info().Msgf("WatchRPCStatus stopped for chain %d", ob.Chain().ChainId) return } } @@ -327,7 +321,7 @@ func (ob *Observer) WatchGasPrice() { } ticker.UpdateInterval(ob.GetChainParams().GasPriceTicker, ob.Logger().GasPrice) case <-ob.StopChannel(): - ob.Logger().GasPrice.Info().Msg("WatchGasPrice stopped") + ob.Logger().GasPrice.Info().Msgf("WatchGasPrice stopped for chain %d", ob.Chain().ChainId) return } } diff --git a/zetaclient/chains/evm/observer/observer_test.go b/zetaclient/chains/evm/observer/observer_test.go index e38ff877a1..8daca344c8 100644 --- a/zetaclient/chains/evm/observer/observer_test.go +++ b/zetaclient/chains/evm/observer/observer_test.go @@ -7,6 +7,7 @@ import ( "testing" "cosmossdk.io/math" + "github.com/btcsuite/btcd/chaincfg" ethtypes "github.com/ethereum/go-ethereum/core/types" lru "github.com/hashicorp/golang-lru" "github.com/onrik/ethrpc" @@ -32,8 +33,8 @@ import ( // the relative path to the testdata directory var TestDataDir = "../../../" -// getZetacoreContext creates a zetacore context for unit tests -func getZetacoreContext( +// getAppContext creates a app context for unit tests +func getAppContext( evmChain chains.Chain, endpoint string, evmChainParams *observertypes.ChainParams, @@ -50,25 +51,25 @@ func getZetacoreContext( Endpoint: endpoint, } - // create zetacore context - appContext := context.New(cfg, zerolog.Nop()) - evmChainParamsMap := make(map[int64]*observertypes.ChainParams) - evmChainParamsMap[evmChain.ChainId] = evmChainParams + // create app context + appCtx := context.New(cfg) + newChainParams := make(map[int64]*observertypes.ChainParams) + newChainParams[evmChain.ChainId] = evmChainParams // feed chain params - appContext.Update( - &observertypes.Keygen{}, - []chains.Chain{evmChain}, - evmChainParamsMap, - nil, + appCtx.Update( + observertypes.Keygen{}, "", + []chains.Chain{evmChain}, + newChainParams, + &chaincfg.RegressionNetParams, *sample.CrosschainFlags(), []chains.Chain{}, sample.HeaderSupportedChains(), - true, + zerolog.Logger{}, ) // create app context - return appContext, cfg.EVMChainConfigs[evmChain.ChainId] + return appCtx, cfg.EVMChainConfigs[evmChain.ChainId] } // MockEVMObserver creates a mock ChainObserver with custom chain, TSS, params etc @@ -79,7 +80,6 @@ func MockEVMObserver( evmJSONRPC interfaces.EVMJSONRPCClient, zetacoreClient interfaces.ZetacoreClient, tss interfaces.TSSSigner, - dbpath string, lastBlock uint64, params observertypes.ChainParams, ) *observer.Observer { @@ -96,11 +96,11 @@ func MockEVMObserver( if tss == nil { tss = mocks.NewTSSMainnet() } - // create zetacore context - coreCtx, evmCfg := getZetacoreContext(chain, "", ¶ms) + // create app context + appCtx, evmCfg := getAppContext(chain, "", ¶ms) // create observer - ob, err := observer.NewObserver(evmCfg, evmClient, params, coreCtx, zetacoreClient, tss, dbpath, base.Logger{}, nil) + ob, err := observer.NewObserver(evmCfg, evmClient, params, appCtx, zetacoreClient, tss, base.Logger{}, nil) require.NoError(t, err) ob.WithEvmJSONRPC(evmJSONRPC) ob.WithLastBlock(lastBlock) @@ -120,7 +120,6 @@ func Test_NewObserver(t *testing.T) { chainParams observertypes.ChainParams evmClient interfaces.EVMRPCClient tss interfaces.TSSSigner - dbpath string logger base.Logger ts *metrics.TelemetryServer fail bool @@ -135,48 +134,17 @@ func Test_NewObserver(t *testing.T) { chainParams: params, evmClient: mocks.NewMockEvmClient().WithBlockNumber(1000), tss: mocks.NewTSSMainnet(), - dbpath: sample.CreateTempDir(t), logger: base.Logger{}, ts: nil, fail: false, }, - { - name: "should fail on invalid dbpath", - evmCfg: config.EVMConfig{ - Chain: chain, - Endpoint: "http://localhost:8545", - }, - chainParams: params, - evmClient: mocks.NewMockEvmClient().WithBlockNumber(1000), - tss: mocks.NewTSSMainnet(), - dbpath: "/invalid/dbpath", // invalid dbpath - logger: base.Logger{}, - ts: nil, - fail: true, - message: "error creating db path", - }, - { - name: "should fail if RPC call fails", - evmCfg: config.EVMConfig{ - Chain: chain, - Endpoint: "http://localhost:8545", - }, - chainParams: params, - evmClient: mocks.NewMockEvmClient().WithError(fmt.Errorf("error RPC")), - tss: mocks.NewTSSMainnet(), - dbpath: sample.CreateTempDir(t), - logger: base.Logger{}, - ts: nil, - fail: true, - message: "error RPC", - }, } // run tests 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) + // create app context, client and tss + appCtx, _ := getAppContext(tt.evmCfg.Chain, tt.evmCfg.Endpoint, ¶ms) zetacoreClient := mocks.NewMockZetacoreClient().WithKeys(&keys.Keys{}) // create observer @@ -184,10 +152,9 @@ func Test_NewObserver(t *testing.T) { tt.evmCfg, tt.evmClient, tt.chainParams, - zetacoreCtx, + appCtx, zetacoreClient, tt.tss, - tt.dbpath, tt.logger, tt.ts, ) @@ -209,7 +176,7 @@ func Test_LoadDB(t *testing.T) { chain := chains.Ethereum params := mocks.MockChainParams(chain.ChainId, 10) dbpath := sample.CreateTempDir(t) - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, dbpath, 1, params) + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, 1, params) t.Run("should load db successfully", func(t *testing.T) { err := ob.LoadDB(dbpath) @@ -238,7 +205,7 @@ func Test_LoadDB(t *testing.T) { t.Run("should fail on RPC error", func(t *testing.T) { // create observer tempClient := mocks.NewMockEvmClient() - ob := MockEVMObserver(t, chain, tempClient, nil, nil, nil, dbpath, 1, params) + ob := MockEVMObserver(t, chain, tempClient, nil, nil, nil, 1, params) // set RPC error tempClient.WithError(fmt.Errorf("error RPC")) @@ -257,7 +224,11 @@ func Test_LoadLastBlockScanned(t *testing.T) { // create observer using mock evm client evmClient := mocks.NewMockEvmClient().WithBlockNumber(100) dbpath := sample.CreateTempDir(t) - ob := MockEVMObserver(t, chain, evmClient, nil, nil, nil, dbpath, 1, params) + ob := MockEVMObserver(t, chain, evmClient, nil, nil, nil, 1, params) + + // load db + err := ob.LoadDB(dbpath) + require.NoError(t, err) t.Run("should load last block scanned", func(t *testing.T) { // create db and write 123 as last block scanned @@ -281,7 +252,11 @@ func Test_LoadLastBlockScanned(t *testing.T) { t.Run("should fail on RPC error", func(t *testing.T) { // create observer on separate path, as we need to reset last block scanned otherPath := sample.CreateTempDir(t) - obOther := MockEVMObserver(t, chain, evmClient, nil, nil, nil, otherPath, 1, params) + obOther := MockEVMObserver(t, chain, evmClient, nil, nil, nil, 1, params) + + // load db + err := obOther.LoadDB(otherPath) + require.NoError(t, err) // reset last block scanned to 0 so that it will be loaded from RPC obOther.WithLastBlockScanned(0) @@ -290,7 +265,7 @@ func Test_LoadLastBlockScanned(t *testing.T) { evmClient.WithError(fmt.Errorf("error RPC")) // load last block scanned - err := obOther.LoadLastBlockScanned() + err = obOther.LoadLastBlockScanned() require.ErrorContains(t, err, "error RPC") }) } diff --git a/zetaclient/chains/evm/observer/outbound.go b/zetaclient/chains/evm/observer/outbound.go index 84103620dd..c7e3ddf450 100644 --- a/zetaclient/chains/evm/observer/outbound.go +++ b/zetaclient/chains/evm/observer/outbound.go @@ -89,7 +89,7 @@ func (ob *Observer) WatchOutbound() { } ticker.UpdateInterval(ob.GetChainParams().OutboundTicker, ob.Logger().Outbound) case <-ob.StopChannel(): - ob.Logger().Outbound.Info().Msg("WatchOutbound: stopped") + ob.Logger().Outbound.Info().Msgf("WatchOutbound stopped for chain %d", ob.Chain().ChainId) return } } diff --git a/zetaclient/chains/evm/observer/outbound_test.go b/zetaclient/chains/evm/observer/outbound_test.go index 72023d8f57..411c4c9764 100644 --- a/zetaclient/chains/evm/observer/outbound_test.go +++ b/zetaclient/chains/evm/observer/outbound_test.go @@ -14,13 +14,12 @@ import ( "github.com/zeta-chain/zetacore/testutil/sample" observertypes "github.com/zeta-chain/zetacore/x/observer/types" "github.com/zeta-chain/zetacore/zetaclient/chains/evm/observer" + "github.com/zeta-chain/zetacore/zetaclient/compliance" "github.com/zeta-chain/zetacore/zetaclient/config" "github.com/zeta-chain/zetacore/zetaclient/testutils" "github.com/zeta-chain/zetacore/zetaclient/testutils/mocks" ) -const memDBPath = testutils.SQLiteMemory - // getContractsByChainID is a helper func to get contracts and addresses by chainID func getContractsByChainID( t *testing.T, @@ -60,7 +59,7 @@ func Test_IsOutboundProcessed(t *testing.T) { 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 := MockEVMObserver(t, chain, nil, nil, nil, nil, 1, chainParam) ob.SetTxNReceipt(nonce, receipt, outbound) // post outbound vote @@ -77,7 +76,7 @@ func Test_IsOutboundProcessed(t *testing.T) { cctx.InboundParams.Sender = sample.EthAddress().Hex() // create evm observer and set outbound and receipt - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, 1, chainParam) + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, 1, chainParam) ob.SetTxNReceipt(nonce, receipt, outbound) // modify compliance config to restrict sender address @@ -85,7 +84,7 @@ func Test_IsOutboundProcessed(t *testing.T) { ComplianceConfig: config.ComplianceConfig{}, } cfg.ComplianceConfig.RestrictedAddresses = []string{cctx.InboundParams.Sender} - config.LoadComplianceConfig(cfg) + compliance.LoadComplianceConfig(cfg) // post outbound vote isIncluded, isConfirmed, err := ob.IsOutboundProcessed(cctx, zerolog.Logger{}) @@ -95,7 +94,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) + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, 1, chainParam) isIncluded, isConfirmed, err := ob.IsOutboundProcessed(cctx, zerolog.Logger{}) require.NoError(t, err) require.False(t, isIncluded) @@ -103,7 +102,7 @@ func Test_IsOutboundProcessed(t *testing.T) { }) t.Run("should fail if unable to parse ZetaReceived event", func(t *testing.T) { // create evm observer and set outbound and receipt - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, 1, chainParam) + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, 1, chainParam) ob.SetTxNReceipt(nonce, receipt, outbound) // set connector contract address to an arbitrary address to make event parsing fail @@ -151,7 +150,7 @@ func Test_IsOutboundProcessed_ContractError(t *testing.T) { 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) + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, 1, chainParam) ob.SetTxNReceipt(nonce, receipt, outbound) abiConnector := zetaconnector.ZetaConnectorNonEthMetaData.ABI abiCustody := erc20custody.ERC20CustodyMetaData.ABI @@ -196,7 +195,7 @@ func Test_PostVoteOutbound(t *testing.T) { // 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 := MockEVMObserver(t, chain, nil, nil, zetacoreClient, nil, 1, observertypes.ChainParams{}) ob.PostVoteOutbound( cctx.Index, receipt, diff --git a/zetaclient/chains/evm/signer/signer.go b/zetaclient/chains/evm/signer/signer.go index db4a15c856..e90af35366 100644 --- a/zetaclient/chains/evm/signer/signer.go +++ b/zetaclient/chains/evm/signer/signer.go @@ -30,6 +30,7 @@ import ( "github.com/zeta-chain/zetacore/zetaclient/chains/evm" "github.com/zeta-chain/zetacore/zetaclient/chains/evm/observer" "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" + "github.com/zeta-chain/zetacore/zetaclient/common" "github.com/zeta-chain/zetacore/zetaclient/compliance" clientcontext "github.com/zeta-chain/zetacore/zetaclient/context" "github.com/zeta-chain/zetacore/zetaclient/metrics" @@ -96,7 +97,7 @@ func NewSigner( baseSigner := base.NewSigner(chain, appContext, tss, ts, logger) // create EVM client - client, ethSigner, err := getEVMRPC(endpoint) + client, ethSigner, err := getEVMRPC(endpoint, chain.ChainId) if err != nil { return nil, err } @@ -819,14 +820,14 @@ func (signer *Signer) reportToOutboundTracker( break } // retry otherwise - time.Sleep(evm.ZetaBlockTime * 3) + time.Sleep(common.ZetaBlockTime * 3) } } }() } // 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(endpoint string, chainID int64) (interfaces.EVMRPCClient, ethtypes.Signer, error) { if endpoint == mocks.EVMRPCEnabled { chainID := big.NewInt(chains.BscMainnet.ChainId) ethSigner := ethtypes.NewLondonSigner(chainID) @@ -839,11 +840,7 @@ func getEVMRPC(endpoint string) (interfaces.EVMRPCClient, ethtypes.Signer, error return nil, nil, err } - chainID, err := client.ChainID(context.TODO()) - if err != nil { - return nil, nil, err - } - ethSigner := ethtypes.LatestSignerForChainID(chainID) + ethSigner := ethtypes.LatestSignerForChainID(big.NewInt(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..f682bb15f0 100644 --- a/zetaclient/chains/evm/signer/signer_test.go +++ b/zetaclient/chains/evm/signer/signer_test.go @@ -47,7 +47,7 @@ func getNewEvmSigner(tss interfaces.TSSSigner) (*Signer, error) { return NewSigner( chains.BscMainnet, - context.New(cfg, zerolog.Nop()), + context.New(cfg), tss, nil, logger, @@ -71,8 +71,7 @@ func getNewEvmChainObserver(t *testing.T, tss interfaces.TSSSigner) (*observer.O evmClient := mocks.NewMockEvmClient().WithBlockNumber(1000) params := mocks.MockChainParams(evmcfg.Chain.ChainId, 10) cfg.EVMChainConfigs[chains.BscMainnet.ChainId] = evmcfg - appContext := context.New(cfg, zerolog.Nop()) - dbpath := sample.CreateTempDir(t) + appContext := context.New(cfg) logger := base.Logger{} ts := &metrics.TelemetryServer{} @@ -83,7 +82,6 @@ func getNewEvmChainObserver(t *testing.T, tss interfaces.TSSSigner) (*observer.O appContext, mocks.NewMockZetacoreClient(), tss, - dbpath, logger, ts, ) @@ -484,7 +482,7 @@ func TestSigner_BroadcastOutbound(t *testing.T) { func TestSigner_getEVMRPC(t *testing.T) { t.Run("getEVMRPC error dialing", func(t *testing.T) { - client, signer, err := getEVMRPC("invalidEndpoint") + client, signer, err := getEVMRPC("invalidEndpoint", chains.Ethereum.ChainId) require.Nil(t, client) require.Nil(t, signer) require.Error(t, err) diff --git a/zetaclient/chains/interfaces/interfaces.go b/zetaclient/chains/interfaces/interfaces.go index 2272ef1dfe..8b3cf97921 100644 --- a/zetaclient/chains/interfaces/interfaces.go +++ b/zetaclient/chains/interfaces/interfaces.go @@ -11,6 +11,7 @@ import ( "github.com/btcsuite/btcd/rpcclient" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" + upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" "github.com/ethereum/go-ethereum/accounts/abi/bind" ethcommon "github.com/ethereum/go-ethereum/common" ethtypes "github.com/ethereum/go-ethereum/core/types" @@ -24,6 +25,7 @@ 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" + clientcontext "github.com/zeta-chain/zetacore/zetaclient/context" keyinterfaces "github.com/zeta-chain/zetacore/zetaclient/keys/interfaces" "github.com/zeta-chain/zetacore/zetaclient/outboundprocessor" ) @@ -40,6 +42,9 @@ const ( type ChainObserver interface { Start() Stop() + OpenDB(dbPath string, dbName string) error + LoadDB(dbPath string) error + SaveLastBlockScanned(blockNumber uint64) error IsOutboundProcessed(cctx *crosschaintypes.CrossChainTx, logger zerolog.Logger) (bool, bool, error) SetChainParams(observertypes.ChainParams) GetChainParams() observertypes.ChainParams @@ -65,6 +70,12 @@ type ChainSigner interface { // ZetacoreClient is the client interface to interact with zetacore type ZetacoreClient interface { + UpdateAppContext(appContext *clientcontext.AppContext, logger zerolog.Logger) error + GetUpgradePlan() (*upgradetypes.Plan, error) + GetChainParams() ([]*observertypes.ChainParams, error) + GetSupportedChains() ([]chains.Chain, error) + GetCurrentTss() (observertypes.TSS, error) + GetBlockHeaderEnabledChains() ([]lightclienttypes.HeaderSupportedChain, error) PostVoteInbound(gasLimit, retryGasLimit uint64, msg *crosschaintypes.MsgVoteInbound) (string, string, error) PostVoteOutbound( sendHash string, @@ -110,8 +121,6 @@ type ZetacoreClient interface { GetBtcTssAddress(chainID int64) (string, error) GetZetaHotKeyBalance() (sdkmath.Int, error) GetInboundTrackersForChain(chainID int64) ([]crosschaintypes.InboundTracker, error) - Pause() - Unpause() } // BTCRPCClient is the interface for BTC RPC client diff --git a/zetaclient/common/constant.go b/zetaclient/common/constant.go index 5e410d4886..16438efe4d 100644 --- a/zetaclient/common/constant.go +++ b/zetaclient/common/constant.go @@ -1,6 +1,11 @@ package common +import "time" + const ( + // ZetaBlockTime is the block time of the ZetaChain + ZetaBlockTime = 6000 * time.Millisecond + // EVMOutboundGasPriceMultiplier is the default gas price multiplier for EVM-chain outbond txs EVMOutboundGasPriceMultiplier = 1.2 diff --git a/zetaclient/compliance/compliance.go b/zetaclient/compliance/compliance.go index 849d56742b..9b4dd16077 100644 --- a/zetaclient/compliance/compliance.go +++ b/zetaclient/compliance/compliance.go @@ -2,18 +2,39 @@ package compliance import ( + "strings" + "github.com/rs/zerolog" crosschaintypes "github.com/zeta-chain/zetacore/x/crosschain/types" "github.com/zeta-chain/zetacore/zetaclient/config" ) +// restrictedAddressBook is a map of restricted addresses +var restrictedAddressBook = map[string]bool{} + +// LoadComplianceConfig loads compliance config from zetaclient config +func LoadComplianceConfig(cfg config.Config) { + restrictedAddressBook = cfg.GetRestrictedAddressBook() +} + +// ContainRestrictedAddress returns true if any one of the addresses is restricted +// Note: the addrs can contains both ETH and BTC addresses +func ContainRestrictedAddress(addrs ...string) bool { + for _, addr := range addrs { + if addr != "" && restrictedAddressBook[strings.ToLower(addr)] { + return true + } + } + return false +} + // IsCctxRestricted returns true if the cctx involves restricted addresses func IsCctxRestricted(cctx *crosschaintypes.CrossChainTx) bool { sender := cctx.InboundParams.Sender receiver := cctx.GetCurrentOutboundParam().Receiver - return config.ContainRestrictedAddress(sender, receiver) + return ContainRestrictedAddress(sender, receiver) } // PrintComplianceLog prints compliance log with fields [chain, cctx/inbound, chain, sender, receiver, token] diff --git a/zetaclient/compliance/compliance_test.go b/zetaclient/compliance/compliance_test.go index 0f587ceb20..7f31243b9a 100644 --- a/zetaclient/compliance/compliance_test.go +++ b/zetaclient/compliance/compliance_test.go @@ -23,29 +23,29 @@ func TestCctxRestricted(t *testing.T) { t.Run("should return true if sender is restricted", func(t *testing.T) { cfg.ComplianceConfig.RestrictedAddresses = []string{cctx.InboundParams.Sender} - config.LoadComplianceConfig(cfg) + LoadComplianceConfig(cfg) require.True(t, IsCctxRestricted(cctx)) }) t.Run("should return true if receiver is restricted", func(t *testing.T) { cfg.ComplianceConfig.RestrictedAddresses = []string{cctx.GetCurrentOutboundParam().Receiver} - config.LoadComplianceConfig(cfg) + LoadComplianceConfig(cfg) require.True(t, IsCctxRestricted(cctx)) }) t.Run("should return false if sender and receiver are not restricted", func(t *testing.T) { // restrict other address cfg.ComplianceConfig.RestrictedAddresses = []string{"0x27104b8dB4aEdDb054fCed87c346C0758Ff5dFB1"} - config.LoadComplianceConfig(cfg) + LoadComplianceConfig(cfg) require.False(t, IsCctxRestricted(cctx)) }) t.Run("should be able to restrict coinbase address", func(t *testing.T) { cfg.ComplianceConfig.RestrictedAddresses = []string{ethcommon.Address{}.String()} - config.LoadComplianceConfig(cfg) + LoadComplianceConfig(cfg) cctx.InboundParams.Sender = ethcommon.Address{}.String() require.True(t, IsCctxRestricted(cctx)) }) t.Run("should ignore empty address", func(t *testing.T) { cfg.ComplianceConfig.RestrictedAddresses = []string{""} - config.LoadComplianceConfig(cfg) + LoadComplianceConfig(cfg) cctx.InboundParams.Sender = "" require.False(t, IsCctxRestricted(cctx)) }) diff --git a/zetaclient/config/config.go b/zetaclient/config/config.go index 6efd149628..e91cf24a78 100644 --- a/zetaclient/config/config.go +++ b/zetaclient/config/config.go @@ -9,9 +9,6 @@ import ( "strings" ) -// restrictedAddressBook is a map of restricted addresses -var restrictedAddressBook = map[string]bool{} - // filename is config file name for ZetaClient const filename string = "zetaclient_config.json" @@ -76,17 +73,9 @@ func Load(path string) (Config, error) { cfg.PreParamsPath = GetPath(cfg.PreParamsPath) cfg.ZetaCoreHome = path - // load compliance config - LoadComplianceConfig(cfg) - return cfg, nil } -// LoadComplianceConfig loads compliance data (restricted addresses) from config -func LoadComplianceConfig(cfg Config) { - restrictedAddressBook = cfg.GetRestrictedAddressBook() -} - // GetPath returns the absolute path of the input path func GetPath(inputPath string) string { path := strings.Split(inputPath, "/") @@ -103,14 +92,3 @@ func GetPath(inputPath string) string { return inputPath } - -// ContainRestrictedAddress returns true if any one of the addresses is restricted -// Note: the addrs can contains both ETH and BTC addresses -func ContainRestrictedAddress(addrs ...string) bool { - for _, addr := range addrs { - if addr != "" && restrictedAddressBook[strings.ToLower(addr)] { - return true - } - } - return false -} diff --git a/zetaclient/config/config_chain.go b/zetaclient/config/config_chain.go index 5946c4ca62..6e15211156 100644 --- a/zetaclient/config/config_chain.go +++ b/zetaclient/config/config_chain.go @@ -1,8 +1,6 @@ package config import ( - "sync" - "github.com/zeta-chain/zetacore/pkg/chains" ) @@ -36,7 +34,6 @@ func GetERC20CustodyABI() string { // It is initialize with default chain configs func New() Config { return Config{ - cfgLock: &sync.RWMutex{}, EVMChainConfigs: evmChainsConfigs, BitcoinConfig: bitcoinConfigRegnet, } diff --git a/zetaclient/config/types.go b/zetaclient/config/types.go index 96cdf24a4c..b83e3cc814 100644 --- a/zetaclient/config/types.go +++ b/zetaclient/config/types.go @@ -3,7 +3,6 @@ package config import ( "encoding/json" "strings" - "sync" "github.com/zeta-chain/zetacore/pkg/chains" ) @@ -57,8 +56,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"` @@ -90,24 +87,15 @@ type Config struct { // TODO(revamp): consolidate with New function func NewConfig() Config { return Config{ - cfgLock: &sync.RWMutex{}, EVMChainConfigs: make(map[int64]EVMConfig), + ComplianceConfig: ComplianceConfig{ + RestrictedAddresses: make([]string, 0), + }, } } -// 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() - 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() - // deep copy evm configs copied := make(map[int64]EVMConfig, len(c.EVMChainConfigs)) for chainID, evmConfig := range c.EVMChainConfigs { @@ -118,9 +106,6 @@ 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() - return c.BitcoinConfig, c.BitcoinConfig != (BTCConfig{}) } @@ -145,9 +130,27 @@ func (c Config) GetRestrictedAddressBook() map[string]bool { return restrictedAddresses } -// GetKeyringBackend returns the keyring backend -func (c *Config) GetKeyringBackend() KeyringBackend { - c.cfgLock.RLock() - defer c.cfgLock.RUnlock() - return c.KeyringBackend +// Clone returns a deep copy of the config +// config is accessed concurrently, so a deep copy is needed +func (c Config) Clone() Config { + // deep copy evm config map + copiedEVMConfigs := make(map[int64]EVMConfig, len(c.EVMChainConfigs)) + for chainID, evmConfig := range c.EVMChainConfigs { + copiedEVMConfigs[chainID] = evmConfig + } + + // deep copy compliance config address array + copiedComplianceConfig := c.ComplianceConfig + copiedRestrictedAddresses := make([]string, len(copiedComplianceConfig.RestrictedAddresses)) + copy(copiedRestrictedAddresses, copiedComplianceConfig.RestrictedAddresses) + copiedComplianceConfig.RestrictedAddresses = copiedRestrictedAddresses + + // duplicate a config + copied := c + + // set deep copied evm configs, and compliance config + copied.EVMChainConfigs = copiedEVMConfigs + copied.ComplianceConfig = copiedComplianceConfig + + return copied } diff --git a/zetaclient/config/types_test.go b/zetaclient/config/types_test.go index d7b82b3200..bb5dd35a0e 100644 --- a/zetaclient/config/types_test.go +++ b/zetaclient/config/types_test.go @@ -1 +1,22 @@ package config_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/zeta-chain/zetacore/zetaclient/testutils" +) + +// the relative path to the testdata directory +var TestDataDir = "../" + +func Test_Clone(t *testing.T) { + // read archived zetaclient config file + cfg := testutils.LoadZetaclientConfig(t, TestDataDir) + + // clone the config + clone := cfg.Clone() + + // assert that the cloned config is equal to the original config + require.Equal(t, cfg, clone) +} diff --git a/zetaclient/context/app.go b/zetaclient/context/app.go deleted file mode 100644 index 4888443ea9..0000000000 --- a/zetaclient/context/app.go +++ /dev/null @@ -1,300 +0,0 @@ -// Package context provides global app context for ZetaClient -package context - -import ( - "sort" - "sync" - - "github.com/rs/zerolog" - - "github.com/zeta-chain/zetacore/pkg/chains" - lightclienttypes "github.com/zeta-chain/zetacore/x/lightclient/types" - observertypes "github.com/zeta-chain/zetacore/x/observer/types" - "github.com/zeta-chain/zetacore/zetaclient/config" -) - -// AppContext represents application context. -type AppContext struct { - config config.Config - logger zerolog.Logger - - keygen observertypes.Keygen - chainsEnabled []chains.Chain - evmChainParams map[int64]*observertypes.ChainParams - bitcoinChainParams *observertypes.ChainParams - currentTssPubkey string - crosschainFlags observertypes.CrosschainFlags - - // additionalChains is a list of additional static chain information to use when searching from chain IDs - // it is stored in the protocol to dynamically support new chains without doing an upgrade - additionalChain []chains.Chain - - // blockHeaderEnabledChains is used to store the list of chains that have block header verification enabled - // All chains in this list will have Enabled flag set to true - blockHeaderEnabledChains []lightclienttypes.HeaderSupportedChain - - mu sync.RWMutex -} - -// New creates and returns new AppContext -func New(cfg config.Config, logger zerolog.Logger) *AppContext { - evmChainParams := make(map[int64]*observertypes.ChainParams) - for _, e := range cfg.EVMChainConfigs { - evmChainParams[e.Chain.ChainId] = &observertypes.ChainParams{} - } - - var bitcoinChainParams *observertypes.ChainParams - _, found := cfg.GetBTCConfig() - if found { - bitcoinChainParams = &observertypes.ChainParams{} - } - - return &AppContext{ - config: cfg, - logger: logger.With().Str("module", "appcontext").Logger(), - - chainsEnabled: []chains.Chain{}, - evmChainParams: evmChainParams, - bitcoinChainParams: bitcoinChainParams, - crosschainFlags: observertypes.CrosschainFlags{}, - blockHeaderEnabledChains: []lightclienttypes.HeaderSupportedChain{}, - - currentTssPubkey: "", - keygen: observertypes.Keygen{}, - mu: sync.RWMutex{}, - } -} - -// Config returns the config of the app -func (a *AppContext) Config() config.Config { - return a.config -} - -// GetBTCChainAndConfig returns btc chain and config if enabled -func (a *AppContext) GetBTCChainAndConfig() (chains.Chain, config.BTCConfig, bool) { - btcConfig, configEnabled := a.Config().GetBTCConfig() - btcChain, _, paramsEnabled := a.GetBTCChainParams() - - if !configEnabled || !paramsEnabled { - return chains.Chain{}, config.BTCConfig{}, false - } - - return btcChain, btcConfig, true -} - -// IsOutboundObservationEnabled returns true if the chain is supported and outbound flag is enabled -func (a *AppContext) IsOutboundObservationEnabled(chainParams observertypes.ChainParams) bool { - flags := a.GetCrossChainFlags() - return chainParams.IsSupported && flags.IsOutboundEnabled -} - -// IsInboundObservationEnabled returns true if the chain is supported and inbound flag is enabled -func (a *AppContext) IsInboundObservationEnabled(chainParams observertypes.ChainParams) bool { - flags := a.GetCrossChainFlags() - return chainParams.IsSupported && flags.IsInboundEnabled -} - -// GetKeygen returns the current keygen -func (a *AppContext) GetKeygen() observertypes.Keygen { - a.mu.RLock() - defer a.mu.RUnlock() - - var copiedPubkeys []string - if a.keygen.GranteePubkeys != nil { - copiedPubkeys = make([]string, len(a.keygen.GranteePubkeys)) - copy(copiedPubkeys, a.keygen.GranteePubkeys) - } - - return observertypes.Keygen{ - Status: a.keygen.Status, - GranteePubkeys: copiedPubkeys, - BlockNumber: a.keygen.BlockNumber, - } -} - -// GetCurrentTssPubKey returns the current tss pubkey -func (a *AppContext) GetCurrentTssPubKey() string { - a.mu.RLock() - defer a.mu.RUnlock() - - return a.currentTssPubkey -} - -// GetEnabledChains returns all enabled chains including zetachain -func (a *AppContext) GetEnabledChains() []chains.Chain { - a.mu.RLock() - defer a.mu.RUnlock() - - copiedChains := make([]chains.Chain, len(a.chainsEnabled)) - copy(copiedChains, a.chainsEnabled) - - return copiedChains -} - -// GetEnabledExternalChains returns all enabled external chains -func (a *AppContext) GetEnabledExternalChains() []chains.Chain { - a.mu.RLock() - defer a.mu.RUnlock() - - externalChains := make([]chains.Chain, 0) - for _, chain := range a.chainsEnabled { - if chain.IsExternal { - externalChains = append(externalChains, chain) - } - } - return externalChains -} - -// GetEVMChainParams returns chain params for a specific EVM chain -func (a *AppContext) GetEVMChainParams(chainID int64) (*observertypes.ChainParams, bool) { - a.mu.RLock() - defer a.mu.RUnlock() - - evmChainParams, found := a.evmChainParams[chainID] - return evmChainParams, found -} - -// GetAllEVMChainParams returns all chain params for EVM chains -func (a *AppContext) GetAllEVMChainParams() map[int64]*observertypes.ChainParams { - a.mu.RLock() - defer a.mu.RUnlock() - - // deep copy evm chain params - copied := make(map[int64]*observertypes.ChainParams, len(a.evmChainParams)) - for chainID, evmConfig := range a.evmChainParams { - copied[chainID] = &observertypes.ChainParams{} - *copied[chainID] = *evmConfig - } - return copied -} - -// GetBTCChainParams returns (chain, chain params, found) for bitcoin chain -func (a *AppContext) GetBTCChainParams() (chains.Chain, *observertypes.ChainParams, bool) { - a.mu.RLock() - defer a.mu.RUnlock() - - if a.bitcoinChainParams == nil { // bitcoin is not enabled - return chains.Chain{}, nil, false - } - - chain, found := chains.GetChainFromChainID(a.bitcoinChainParams.ChainId, a.additionalChain) - if !found { - return chains.Chain{}, nil, false - } - - return chain, a.bitcoinChainParams, true -} - -// GetCrossChainFlags returns crosschain flags -func (a *AppContext) GetCrossChainFlags() observertypes.CrosschainFlags { - a.mu.RLock() - defer a.mu.RUnlock() - - return a.crosschainFlags -} - -// GetAdditionalChains returns additional chains -func (a *AppContext) GetAdditionalChains() []chains.Chain { - a.mu.RLock() - defer a.mu.RUnlock() - return a.additionalChain -} - -// GetAllHeaderEnabledChains returns all verification flags -func (a *AppContext) GetAllHeaderEnabledChains() []lightclienttypes.HeaderSupportedChain { - a.mu.RLock() - defer a.mu.RUnlock() - - return a.blockHeaderEnabledChains -} - -// GetBlockHeaderEnabledChains checks if block header verification is enabled for a specific chain -func (a *AppContext) GetBlockHeaderEnabledChains(chainID int64) (lightclienttypes.HeaderSupportedChain, bool) { - a.mu.RLock() - defer a.mu.RUnlock() - - for _, flags := range a.blockHeaderEnabledChains { - if flags.ChainId == chainID { - return flags, true - } - } - - return lightclienttypes.HeaderSupportedChain{}, false -} - -// Update updates zetacore context and params for all chains -// this must be the ONLY function that writes to zetacore context -func (a *AppContext) Update( - keygen *observertypes.Keygen, - newChains []chains.Chain, - evmChainParams map[int64]*observertypes.ChainParams, - btcChainParams *observertypes.ChainParams, - tssPubKey string, - crosschainFlags observertypes.CrosschainFlags, - additionalChains []chains.Chain, - blockHeaderEnabledChains []lightclienttypes.HeaderSupportedChain, - init bool, -) { - // Ignore whatever order zetacore organizes chain list in state - sort.SliceStable(newChains, func(i, j int) bool { - return newChains[i].ChainId < newChains[j].ChainId - }) - - if len(newChains) == 0 { - a.logger.Warn().Msg("UpdateChainParams: No chains enabled in ZeroCore") - } - - a.mu.Lock() - defer a.mu.Unlock() - - // Add some warnings if chain list changes at runtime - if !init && !chainsEqual(a.chainsEnabled, newChains) { - a.logger.Warn(). - Interface("chains.current", a.chainsEnabled). - Interface("chains.new", newChains). - Msg("UpdateChainParams: ChainsEnabled changed at runtime!") - } - - if keygen != nil { - a.keygen = *keygen - } - - a.chainsEnabled = newChains - a.crosschainFlags = crosschainFlags - a.additionalChain = additionalChains - a.blockHeaderEnabledChains = blockHeaderEnabledChains - - // update chain params for bitcoin if it has config in file - if a.bitcoinChainParams != nil && btcChainParams != nil { - a.bitcoinChainParams = btcChainParams - } - - // update core params for evm chains we have configs in file - for _, params := range evmChainParams { - _, found := a.evmChainParams[params.ChainId] - if !found { - continue - } - a.evmChainParams[params.ChainId] = params - } - - if tssPubKey != "" { - a.currentTssPubkey = tssPubKey - } -} - -func chainsEqual(a []chains.Chain, b []chains.Chain) bool { - if len(a) != len(b) { - return false - } - - for i, left := range a { - right := b[i] - - if left.ChainId != right.ChainId || left.ChainName != right.ChainName { - return false - } - } - - return true -} diff --git a/zetaclient/context/app_context.go b/zetaclient/context/app_context.go new file mode 100644 index 0000000000..b2cb5b2c4f --- /dev/null +++ b/zetaclient/context/app_context.go @@ -0,0 +1,242 @@ +// Package context provides global app context for ZetaClient +package context + +import ( + "sort" + "sync" + + "github.com/btcsuite/btcd/chaincfg" + "github.com/rs/zerolog" + + "github.com/zeta-chain/zetacore/pkg/chains" + lightclienttypes "github.com/zeta-chain/zetacore/x/lightclient/types" + observertypes "github.com/zeta-chain/zetacore/x/observer/types" + "github.com/zeta-chain/zetacore/zetaclient/config" +) + +// AppContext contains zetaclient application context +// these are initialized and updated at runtime periodically +type AppContext struct { + config config.Config + keygen observertypes.Keygen + currentTssPubkey string + chainsEnabled []chains.Chain + chainParamMap map[int64]*observertypes.ChainParams + btcNetParams *chaincfg.Params + crosschainFlags observertypes.CrosschainFlags + + // additionalChains is a list of additional static chain information to use when searching from chain IDs + // it is stored in the protocol to dynamically support new chains without doing an upgrade + additionalChain []chains.Chain + + // blockHeaderEnabledChains is used to store the list of chains that have block header verification enabled + // All chains in this list will have Enabled flag set to true + blockHeaderEnabledChains []lightclienttypes.HeaderSupportedChain + + // mu is to protect the app context from concurrent access + mu sync.RWMutex +} + +// New creates empty app context with given config +func New(cfg config.Config) *AppContext { + return &AppContext{ + config: cfg, + chainsEnabled: []chains.Chain{}, + chainParamMap: make(map[int64]*observertypes.ChainParams), + crosschainFlags: observertypes.CrosschainFlags{}, + additionalChain: []chains.Chain{}, + blockHeaderEnabledChains: []lightclienttypes.HeaderSupportedChain{}, + mu: sync.RWMutex{}, + } +} + +// SetConfig sets a new config to the app context +func (a *AppContext) SetConfig(cfg config.Config) { + a.mu.Lock() + defer a.mu.Unlock() + a.config = cfg +} + +// Config returns the app context config +func (a *AppContext) Config() config.Config { + a.mu.RLock() + defer a.mu.RUnlock() + return a.config.Clone() +} + +// GetKeygen returns the current keygen information +func (a *AppContext) GetKeygen() observertypes.Keygen { + a.mu.RLock() + defer a.mu.RUnlock() + + var copiedPubkeys []string + if a.keygen.GranteePubkeys != nil { + copiedPubkeys = make([]string, len(a.keygen.GranteePubkeys)) + copy(copiedPubkeys, a.keygen.GranteePubkeys) + } + + return observertypes.Keygen{ + Status: a.keygen.Status, + GranteePubkeys: copiedPubkeys, + BlockNumber: a.keygen.BlockNumber, + } +} + +// GetCurrentTssPubkey returns the current TSS public key +func (a *AppContext) GetCurrentTssPubkey() string { + a.mu.RLock() + defer a.mu.RUnlock() + return a.currentTssPubkey +} + +// GetEnabledExternalChains returns all enabled external chains (excluding zetachain) +func (a *AppContext) GetEnabledExternalChains() []chains.Chain { + a.mu.RLock() + defer a.mu.RUnlock() + + // deep copy chains + externalChains := make([]chains.Chain, 0) + for _, chain := range a.chainsEnabled { + if chain.IsExternal { + externalChains = append(externalChains, chain) + } + } + return externalChains +} + +// GetEnabledBTCChains returns the enabled bitcoin chains +func (a *AppContext) GetEnabledBTCChains() []chains.Chain { + a.mu.RLock() + defer a.mu.RUnlock() + + // deep copy btc chains + btcChains := make([]chains.Chain, 0) + for _, chain := range a.chainsEnabled { + if chain.Consensus == chains.Consensus_bitcoin { + btcChains = append(btcChains, chain) + } + } + return btcChains +} + +// GetEnabledExternalChainParams returns all enabled chain params +func (a *AppContext) GetEnabledExternalChainParams() map[int64]*observertypes.ChainParams { + a.mu.RLock() + defer a.mu.RUnlock() + + // deep copy chain params + copied := make(map[int64]*observertypes.ChainParams, len(a.chainParamMap)) + for chainID, chainParams := range a.chainParamMap { + copied[chainID] = &observertypes.ChainParams{} + *copied[chainID] = *chainParams + } + return copied +} + +// GetExternalChainParams returns chain params for a specific chain ID +func (a *AppContext) GetExternalChainParams(chainID int64) (*observertypes.ChainParams, bool) { + a.mu.RLock() + defer a.mu.RUnlock() + + chainParams, found := a.chainParamMap[chainID] + return chainParams, found +} + +// GetBTCNetParams returns bitcoin network params +func (a *AppContext) GetBTCNetParams() *chaincfg.Params { + a.mu.RLock() + defer a.mu.RUnlock() + return a.btcNetParams +} + +// GetCrossChainFlags returns crosschain flags +func (a *AppContext) GetCrossChainFlags() observertypes.CrosschainFlags { + a.mu.RLock() + defer a.mu.RUnlock() + return a.crosschainFlags +} + +// GetAdditionalChains returns additional chains +func (a *AppContext) GetAdditionalChains() []chains.Chain { + a.mu.RLock() + defer a.mu.RUnlock() + + // deep copy additional chains + additionalChains := make([]chains.Chain, len(a.additionalChain)) + copy(additionalChains, a.additionalChain) + + return a.additionalChain +} + +// GetBlockHeaderEnabledChains checks if block header verification is enabled for a specific chain +func (a *AppContext) GetBlockHeaderEnabledChains(chainID int64) (lightclienttypes.HeaderSupportedChain, bool) { + a.mu.RLock() + defer a.mu.RUnlock() + for _, flags := range a.blockHeaderEnabledChains { + if flags.ChainId == chainID { + return flags, true + } + } + return lightclienttypes.HeaderSupportedChain{}, false +} + +// Update updates app context and params for all chains +// this must be the ONLY function that writes to app context +func (a *AppContext) Update( + keygen observertypes.Keygen, + tssPubKey string, + chainsEnabled []chains.Chain, + chainParamMap map[int64]*observertypes.ChainParams, + btcNetParams *chaincfg.Params, + crosschainFlags observertypes.CrosschainFlags, + additionalChains []chains.Chain, + blockHeaderEnabledChains []lightclienttypes.HeaderSupportedChain, + logger zerolog.Logger, +) { + a.mu.Lock() + defer a.mu.Unlock() + + // Ignore whatever order zetacore organizes chain list in state + sort.SliceStable(chainsEnabled, func(i, j int) bool { + return chainsEnabled[i].ChainId < chainsEnabled[j].ChainId + }) + + if len(chainsEnabled) == 0 { + logger.Warn().Msg("UpdateChainParams: no external chain enabled in the zetacore") + } + + // Add log print if the number of enabled chains changes at runtime + if len(a.chainsEnabled) != len(chainsEnabled) { + logger.Info().Msgf( + "UpdateChainParams: number of enabled chains changed at runtime!! before: %d, after: %d", + len(a.chainsEnabled), + len(chainsEnabled), + ) + } + + // btcNetParams points one of [mainnet, testnet, regnet] + // btcNetParams initialize only once and should never change + if a.btcNetParams == nil { + a.btcNetParams = btcNetParams + } + + a.keygen = keygen + a.chainsEnabled = chainsEnabled + a.chainParamMap = chainParamMap + a.currentTssPubkey = tssPubKey + a.crosschainFlags = crosschainFlags + a.additionalChain = additionalChains + a.blockHeaderEnabledChains = blockHeaderEnabledChains +} + +// IsOutboundObservationEnabled returns true if the chain is supported and outbound flag is enabled +func (a *AppContext) IsOutboundObservationEnabled(chainParams observertypes.ChainParams) bool { + flags := a.GetCrossChainFlags() + return chainParams.IsSupported && flags.IsOutboundEnabled +} + +// IsInboundObservationEnabled returns true if the chain is supported and inbound flag is enabled +func (a *AppContext) IsInboundObservationEnabled(chainParams observertypes.ChainParams) bool { + flags := a.GetCrossChainFlags() + return chainParams.IsSupported && flags.IsInboundEnabled +} diff --git a/zetaclient/context/app_context_test.go b/zetaclient/context/app_context_test.go new file mode 100644 index 0000000000..c6dafbcace --- /dev/null +++ b/zetaclient/context/app_context_test.go @@ -0,0 +1,245 @@ +package context_test + +import ( + "testing" + + "github.com/btcsuite/btcd/chaincfg" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + "github.com/stretchr/testify/require" + + "github.com/zeta-chain/zetacore/pkg/chains" + "github.com/zeta-chain/zetacore/testutil/sample" + lightclienttypes "github.com/zeta-chain/zetacore/x/lightclient/types" + observertypes "github.com/zeta-chain/zetacore/x/observer/types" + "github.com/zeta-chain/zetacore/zetaclient/config" + context "github.com/zeta-chain/zetacore/zetaclient/context" +) + +func Test_NewAppContext(t *testing.T) { + t.Run("should create new app context with empty config", func(t *testing.T) { + testCfg := config.NewConfig() + + appContext := context.New(testCfg) + require.NotNil(t, appContext) + + // assert config + require.Equal(t, testCfg, appContext.Config()) + + // assert keygen + keyGen := appContext.GetKeygen() + require.Equal(t, observertypes.Keygen{}, keyGen) + + // assert enabled external chains + require.Empty(t, appContext.GetEnabledExternalChains()) + + // assert external chain params + require.Empty(t, appContext.GetEnabledExternalChainParams()) + + // assert current tss pubkey + require.Equal(t, "", appContext.GetCurrentTssPubkey()) + + // assert crosschain flags + require.Equal(t, observertypes.CrosschainFlags{}, appContext.GetCrossChainFlags()) + + // assert additional chains + require.Empty(t, appContext.GetAdditionalChains()) + }) +} + +func Test_SetGetConfig(t *testing.T) { + t.Run("should create new app context with empty config", func(t *testing.T) { + oldCfg := config.NewConfig() + appContext := context.New(oldCfg) + require.NotNil(t, appContext) + require.Equal(t, oldCfg, appContext.Config()) + + // set new config + evmChain := chains.Ethereum + newCfg := config.NewConfig() + newCfg.EVMChainConfigs[evmChain.ChainId] = config.EVMConfig{ + Chain: evmChain, + } + appContext.SetConfig(newCfg) + require.Equal(t, newCfg, appContext.Config()) + }) +} + +func Test_UpdateAndGetters(t *testing.T) { + // use evm and btc chains for testing + evmChain := chains.Ethereum + btcChain := chains.BitcoinMainnet + + // create sample parameters + keyGen := sample.Keygen(t) + chainsEnabled := []chains.Chain{evmChain, btcChain} + chainParamMap := map[int64]*observertypes.ChainParams{ + evmChain.ChainId: sample.ChainParams(evmChain.ChainId), + btcChain.ChainId: sample.ChainParams(btcChain.ChainId), + } + btcNetParams := &chaincfg.MainNetParams + tssPubKey := "tsspubkeytest" + ccFlags := *sample.CrosschainFlags() + additionalChains := []chains.Chain{ + sample.Chain(1), + sample.Chain(2), + sample.Chain(3), + } + headerSupportedChains := sample.HeaderSupportedChains() + + // feed app context fields + appContext := context.New(config.NewConfig()) + appContext.Update( + *keyGen, + tssPubKey, + chainsEnabled, + chainParamMap, + btcNetParams, + ccFlags, + additionalChains, + headerSupportedChains, + log.Logger, + ) + + t.Run("should get keygen", func(t *testing.T) { + result := appContext.GetKeygen() + require.Equal(t, *keyGen, result) + }) + t.Run("should get current tss pubkey", func(t *testing.T) { + result := appContext.GetCurrentTssPubkey() + require.Equal(t, tssPubKey, result) + }) + t.Run("should get external enabled chains", func(t *testing.T) { + result := appContext.GetEnabledExternalChains() + require.Equal(t, chainsEnabled, result) + }) + t.Run("should get enabled BTC chains", func(t *testing.T) { + result := appContext.GetEnabledBTCChains() + require.Equal(t, []chains.Chain{btcChain}, result) + }) + t.Run("should get enabled external chain params", func(t *testing.T) { + result := appContext.GetEnabledExternalChainParams() + require.Equal(t, chainParamMap, result) + }) + t.Run("should get external chain params by chain id", func(t *testing.T) { + for _, chain := range chainsEnabled { + result, found := appContext.GetExternalChainParams(chain.ChainId) + require.True(t, found) + require.Equal(t, chainParamMap[chain.ChainId], result) + } + }) + t.Run("should get btc network params", func(t *testing.T) { + result := appContext.GetBTCNetParams() + require.Equal(t, btcNetParams, result) + }) + t.Run("should get crosschain flags", func(t *testing.T) { + result := appContext.GetCrossChainFlags() + require.Equal(t, ccFlags, result) + }) + t.Run("should get additional chains", func(t *testing.T) { + result := appContext.GetAdditionalChains() + require.Equal(t, additionalChains, result) + }) + t.Run("should get block header enabled chains", func(t *testing.T) { + for _, chain := range headerSupportedChains { + result, found := appContext.GetBlockHeaderEnabledChains(chain.ChainId) + require.True(t, found) + require.Equal(t, chain, result) + } + }) +} + +func TestIsOutboundObservationEnabled(t *testing.T) { + // create test chain params and flags + evmChain := chains.Ethereum + ccFlags := sample.CrosschainFlags() + verificationFlags := sample.HeaderSupportedChains() + chainParams := &observertypes.ChainParams{ + ChainId: evmChain.ChainId, + IsSupported: true, + } + + t.Run("should return true if chain is supported and outbound flag is enabled", func(t *testing.T) { + appContext := makeAppContext(evmChain, chainParams, *ccFlags, verificationFlags) + + require.True(t, appContext.IsOutboundObservationEnabled(*chainParams)) + }) + t.Run("should return false if chain is not supported yet", func(t *testing.T) { + paramsUnsupported := &observertypes.ChainParams{ChainId: evmChain.ChainId, IsSupported: false} + appContextUnsupported := makeAppContext(evmChain, paramsUnsupported, *ccFlags, verificationFlags) + + require.False(t, appContextUnsupported.IsOutboundObservationEnabled(*paramsUnsupported)) + }) + t.Run("should return false if outbound flag is disabled", func(t *testing.T) { + flagsDisabled := ccFlags + flagsDisabled.IsOutboundEnabled = false + coreContextDisabled := makeAppContext(evmChain, chainParams, *flagsDisabled, verificationFlags) + + require.False(t, coreContextDisabled.IsOutboundObservationEnabled(*chainParams)) + }) +} + +func TestIsInboundObservationEnabled(t *testing.T) { + // create test chain params and flags + evmChain := chains.Ethereum + ccFlags := sample.CrosschainFlags() + verificationFlags := sample.HeaderSupportedChains() + chainParams := &observertypes.ChainParams{ + ChainId: evmChain.ChainId, + IsSupported: true, + } + + t.Run("should return true if chain is supported and inbound flag is enabled", func(t *testing.T) { + appContext := makeAppContext(evmChain, chainParams, *ccFlags, verificationFlags) + + require.True(t, appContext.IsInboundObservationEnabled(*chainParams)) + }) + + t.Run("should return false if chain is not supported yet", func(t *testing.T) { + paramsUnsupported := &observertypes.ChainParams{ChainId: evmChain.ChainId, IsSupported: false} + appContextUnsupported := makeAppContext(evmChain, paramsUnsupported, *ccFlags, verificationFlags) + + require.False(t, appContextUnsupported.IsInboundObservationEnabled(*paramsUnsupported)) + }) + + t.Run("should return false if inbound flag is disabled", func(t *testing.T) { + flagsDisabled := ccFlags + flagsDisabled.IsInboundEnabled = false + appContextDisabled := makeAppContext(evmChain, chainParams, *flagsDisabled, verificationFlags) + + require.False(t, appContextDisabled.IsInboundObservationEnabled(*chainParams)) + }) +} + +// makeAppContext makes a test app context with provided chain params and flags +func makeAppContext( + evmChain chains.Chain, + evmChainParams *observertypes.ChainParams, + ccFlags observertypes.CrosschainFlags, + headerSupportedChains []lightclienttypes.HeaderSupportedChain, +) *context.AppContext { + // create config + cfg := config.NewConfig() + cfg.EVMChainConfigs[evmChain.ChainId] = config.EVMConfig{ + Chain: evmChain, + } + + // create app context + appContext := context.New(cfg) + newChainParams := make(map[int64]*observertypes.ChainParams) + newChainParams[evmChain.ChainId] = evmChainParams + + // feed app context fields + appContext.Update( + observertypes.Keygen{}, + "", + []chains.Chain{evmChain}, + newChainParams, + &chaincfg.RegressionNetParams, + ccFlags, + []chains.Chain{}, + headerSupportedChains, + zerolog.Logger{}, + ) + return appContext +} diff --git a/zetaclient/context/app_test.go b/zetaclient/context/app_test.go deleted file mode 100644 index 640e5d7386..0000000000 --- a/zetaclient/context/app_test.go +++ /dev/null @@ -1,564 +0,0 @@ -package context_test - -import ( - "testing" - - "github.com/rs/zerolog" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/zeta-chain/zetacore/pkg/chains" - "github.com/zeta-chain/zetacore/testutil/sample" - lightclienttypes "github.com/zeta-chain/zetacore/x/lightclient/types" - observertypes "github.com/zeta-chain/zetacore/x/observer/types" - "github.com/zeta-chain/zetacore/zetaclient/config" - "github.com/zeta-chain/zetacore/zetaclient/context" -) - -func TestNew(t *testing.T) { - var ( - testCfg = config.NewConfig() - logger = zerolog.Nop() - ) - - t.Run("should create new zetacore context with empty config", func(t *testing.T) { - appContext := context.New(testCfg, logger) - require.NotNil(t, appContext) - - // assert keygen - keyGen := appContext.GetKeygen() - require.Equal(t, observertypes.Keygen{}, keyGen) - - // assert enabled chains - require.Empty(t, len(appContext.GetEnabledChains())) - - // assert external chains - require.Empty(t, len(appContext.GetEnabledExternalChains())) - - // assert current tss pubkey - require.Equal(t, "", appContext.GetCurrentTssPubKey()) - - // assert btc chain params - chain, btcChainParams, btcChainParamsFound := appContext.GetBTCChainParams() - require.Equal(t, chains.Chain{}, chain) - require.False(t, btcChainParamsFound) - require.Nil(t, btcChainParams) - - // assert evm chain params - allEVMChainParams := appContext.GetAllEVMChainParams() - require.Empty(t, allEVMChainParams) - }) - - 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.BitcoinConfig = config.BTCConfig{ - RPCUsername: "test_user", - RPCPassword: "test_password", - } - - // create zetacore context with 0 chain id - appContext := context.New(testCfg, logger) - require.NotNil(t, appContext) - - // assert btc chain params - chain, btcChainParams, btcChainParamsFound := appContext.GetBTCChainParams() - require.Equal(t, chains.Chain{}, chain) - require.False(t, btcChainParamsFound) - require.Nil(t, btcChainParams) - }) - - t.Run("should create new zetacore context with config containing evm chain params", func(t *testing.T) { - testCfg := config.NewConfig() - testCfg.EVMChainConfigs = map[int64]config.EVMConfig{ - 1: { - Chain: chains.Chain{ - ChainName: 1, - ChainId: 1, - }, - }, - 2: { - Chain: chains.Chain{ - ChainName: 2, - ChainId: 2, - }, - }, - } - appContext := context.New(testCfg, logger) - require.NotNil(t, appContext) - - // assert evm chain params - allEVMChainParams := appContext.GetAllEVMChainParams() - require.Equal(t, 2, len(allEVMChainParams)) - require.Equal(t, &observertypes.ChainParams{}, allEVMChainParams[1]) - require.Equal(t, &observertypes.ChainParams{}, allEVMChainParams[2]) - - evmChainParams1, found := appContext.GetEVMChainParams(1) - require.True(t, found) - require.Equal(t, &observertypes.ChainParams{}, evmChainParams1) - - evmChainParams2, found := appContext.GetEVMChainParams(2) - require.True(t, found) - require.Equal(t, &observertypes.ChainParams{}, evmChainParams2) - }) - - t.Run("should create new zetacore context with config containing btc config", func(t *testing.T) { - testCfg := config.NewConfig() - testCfg.BitcoinConfig = config.BTCConfig{ - RPCUsername: "test username", - RPCPassword: "test password", - RPCHost: "test host", - RPCParams: "test params", - } - appContext := context.New(testCfg, logger) - require.NotNil(t, appContext) - }) -} - -func TestAppContextUpdate(t *testing.T) { - var ( - testCfg = config.NewConfig() - logger = zerolog.Nop() - ) - - t.Run("should update zetacore context after being created from empty config", func(t *testing.T) { - appContext := context.New(testCfg, logger) - require.NotNil(t, appContext) - - keyGenToUpdate := observertypes.Keygen{ - Status: observertypes.KeygenStatus_KeyGenSuccess, - GranteePubkeys: []string{"testpubkey1"}, - } - enabledChainsToUpdate := []chains.Chain{ - { - ChainName: 1, - ChainId: 1, - IsExternal: true, - }, - { - ChainName: 2, - ChainId: 2, - IsExternal: true, - }, - chains.ZetaChainTestnet, - } - evmChainParamsToUpdate := map[int64]*observertypes.ChainParams{ - 1: { - ChainId: 1, - }, - 2: { - ChainId: 2, - }, - } - btcChainParamsToUpdate := &observertypes.ChainParams{ - ChainId: 3, - } - tssPubKeyToUpdate := "tsspubkeytest" - crosschainFlags := sample.CrosschainFlags() - verificationFlags := sample.HeaderSupportedChains() - - require.NotNil(t, crosschainFlags) - appContext.Update( - &keyGenToUpdate, - enabledChainsToUpdate, - evmChainParamsToUpdate, - btcChainParamsToUpdate, - tssPubKeyToUpdate, - *crosschainFlags, - []chains.Chain{}, - verificationFlags, - false, - ) - - // assert keygen updated - keyGen := appContext.GetKeygen() - require.Equal(t, keyGenToUpdate, keyGen) - - // assert enabled chains updated - require.Equal(t, enabledChainsToUpdate, appContext.GetEnabledChains()) - - // assert enabled external chains - require.Equal(t, enabledChainsToUpdate[0:2], appContext.GetEnabledExternalChains()) - - // assert current tss pubkey updated - require.Equal(t, tssPubKeyToUpdate, appContext.GetCurrentTssPubKey()) - - // assert btc chain params still empty because they were not specified in config - chain, btcChainParams, btcChainParamsFound := appContext.GetBTCChainParams() - require.Equal(t, chains.Chain{}, chain) - require.False(t, btcChainParamsFound) - require.Nil(t, btcChainParams) - - // assert evm chain params still empty because they were not specified in config - allEVMChainParams := appContext.GetAllEVMChainParams() - require.Empty(t, allEVMChainParams) - - ccFlags := appContext.GetCrossChainFlags() - require.Equal(t, *crosschainFlags, ccFlags) - - verFlags := appContext.GetAllHeaderEnabledChains() - require.Equal(t, verificationFlags, verFlags) - }) - - 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.EVMChainConfigs = map[int64]config.EVMConfig{ - 1: { - Chain: chains.Chain{ - ChainName: 1, - ChainId: 1, - }, - }, - 2: { - Chain: chains.Chain{ - ChainName: 2, - ChainId: 2, - }, - }, - } - testCfg.BitcoinConfig = config.BTCConfig{ - RPCUsername: "test username", - RPCPassword: "test password", - RPCHost: "test host", - RPCParams: "test params", - } - - appContext := context.New(testCfg, logger) - require.NotNil(t, appContext) - - keyGenToUpdate := observertypes.Keygen{ - Status: observertypes.KeygenStatus_KeyGenSuccess, - GranteePubkeys: []string{"testpubkey1"}, - } - enabledChainsToUpdate := []chains.Chain{ - { - ChainName: 1, - ChainId: 1, - }, - { - ChainName: 2, - ChainId: 2, - }, - } - evmChainParamsToUpdate := map[int64]*observertypes.ChainParams{ - 1: { - ChainId: 1, - }, - 2: { - ChainId: 2, - }, - } - - testBtcChain := chains.BitcoinTestnet - btcChainParamsToUpdate := &observertypes.ChainParams{ - ChainId: testBtcChain.ChainId, - } - tssPubKeyToUpdate := "tsspubkeytest" - crosschainFlags := sample.CrosschainFlags() - verificationFlags := sample.HeaderSupportedChains() - require.NotNil(t, crosschainFlags) - appContext.Update( - &keyGenToUpdate, - enabledChainsToUpdate, - evmChainParamsToUpdate, - btcChainParamsToUpdate, - tssPubKeyToUpdate, - *crosschainFlags, - []chains.Chain{}, - verificationFlags, - false, - ) - - // assert keygen updated - keyGen := appContext.GetKeygen() - require.Equal(t, keyGenToUpdate, keyGen) - - // assert enabled chains updated - require.Equal(t, enabledChainsToUpdate, appContext.GetEnabledChains()) - - // assert current tss pubkey updated - require.Equal(t, tssPubKeyToUpdate, appContext.GetCurrentTssPubKey()) - - // assert btc chain params - chain, btcChainParams, btcChainParamsFound := appContext.GetBTCChainParams() - require.Equal(t, testBtcChain, chain) - require.True(t, btcChainParamsFound) - require.Equal(t, btcChainParamsToUpdate, btcChainParams) - - // assert evm chain params - allEVMChainParams := appContext.GetAllEVMChainParams() - require.Equal(t, evmChainParamsToUpdate, allEVMChainParams) - - evmChainParams1, found := appContext.GetEVMChainParams(1) - require.True(t, found) - require.Equal(t, evmChainParamsToUpdate[1], evmChainParams1) - - evmChainParams2, found := appContext.GetEVMChainParams(2) - require.True(t, found) - require.Equal(t, evmChainParamsToUpdate[2], evmChainParams2) - - ccFlags := appContext.GetCrossChainFlags() - require.Equal(t, ccFlags, *crosschainFlags) - - verFlags := appContext.GetAllHeaderEnabledChains() - require.Equal(t, verFlags, verificationFlags) - }, - ) -} - -func TestIsOutboundObservationEnabled(t *testing.T) { - // create test chain params and flags - evmChain := chains.Ethereum - ccFlags := *sample.CrosschainFlags() - verificationFlags := sample.HeaderSupportedChains() - chainParams := &observertypes.ChainParams{ - ChainId: evmChain.ChainId, - IsSupported: true, - } - - t.Run("should return true if chain is supported and outbound flag is enabled", func(t *testing.T) { - appContext := makeAppContext(evmChain, chainParams, ccFlags, verificationFlags) - - require.True(t, appContext.IsOutboundObservationEnabled(*chainParams)) - }) - t.Run("should return false if chain is not supported yet", func(t *testing.T) { - paramsUnsupported := &observertypes.ChainParams{ChainId: evmChain.ChainId, IsSupported: false} - appContextUnsupported := makeAppContext(evmChain, paramsUnsupported, ccFlags, verificationFlags) - - require.False(t, appContextUnsupported.IsOutboundObservationEnabled(*paramsUnsupported)) - }) - t.Run("should return false if outbound flag is disabled", func(t *testing.T) { - flagsDisabled := ccFlags - flagsDisabled.IsOutboundEnabled = false - coreContextDisabled := makeAppContext(evmChain, chainParams, flagsDisabled, verificationFlags) - - require.False(t, coreContextDisabled.IsOutboundObservationEnabled(*chainParams)) - }) -} - -func TestIsInboundObservationEnabled(t *testing.T) { - // create test chain params and flags - evmChain := chains.Ethereum - ccFlags := *sample.CrosschainFlags() - verificationFlags := sample.HeaderSupportedChains() - chainParams := &observertypes.ChainParams{ - ChainId: evmChain.ChainId, - IsSupported: true, - } - - t.Run("should return true if chain is supported and inbound flag is enabled", func(t *testing.T) { - appContext := makeAppContext(evmChain, chainParams, ccFlags, verificationFlags) - - require.True(t, appContext.IsInboundObservationEnabled(*chainParams)) - }) - - t.Run("should return false if chain is not supported yet", func(t *testing.T) { - paramsUnsupported := &observertypes.ChainParams{ChainId: evmChain.ChainId, IsSupported: false} - appContextUnsupported := makeAppContext(evmChain, paramsUnsupported, ccFlags, verificationFlags) - - require.False(t, appContextUnsupported.IsInboundObservationEnabled(*paramsUnsupported)) - }) - - t.Run("should return false if inbound flag is disabled", func(t *testing.T) { - flagsDisabled := ccFlags - flagsDisabled.IsInboundEnabled = false - appContextDisabled := makeAppContext(evmChain, chainParams, flagsDisabled, verificationFlags) - - require.False(t, appContextDisabled.IsInboundObservationEnabled(*chainParams)) - }) -} - -func TestGetBTCChainAndConfig(t *testing.T) { - logger := zerolog.Nop() - - emptyConfig := config.NewConfig() - nonEmptyConfig := config.New() - - assertEmpty := func(t *testing.T, chain chains.Chain, btcConfig config.BTCConfig, enabled bool) { - assert.Empty(t, chain) - assert.Empty(t, btcConfig) - assert.False(t, enabled) - } - - for _, tt := range []struct { - name string - cfg config.Config - setup func(app *context.AppContext) - assert func(t *testing.T, chain chains.Chain, btcConfig config.BTCConfig, enabled bool) - }{ - { - name: "no btc config", - cfg: emptyConfig, - setup: nil, - assert: assertEmpty, - }, - { - name: "btc config exists, but not chain params are set", - cfg: nonEmptyConfig, - setup: nil, - assert: assertEmpty, - }, - { - name: "btc config exists but chain is invalid", - cfg: nonEmptyConfig, - setup: func(app *context.AppContext) { - app.Update( - &observertypes.Keygen{}, - []chains.Chain{}, - nil, - &observertypes.ChainParams{ChainId: 123}, - "", - observertypes.CrosschainFlags{}, - []chains.Chain{}, - nil, - true, - ) - }, - assert: assertEmpty, - }, - { - name: "btc config exists and chain params are set", - cfg: nonEmptyConfig, - setup: func(app *context.AppContext) { - app.Update( - &observertypes.Keygen{}, - []chains.Chain{}, - nil, - &observertypes.ChainParams{ChainId: chains.BitcoinMainnet.ChainId}, - "", - observertypes.CrosschainFlags{}, - []chains.Chain{}, - nil, - true, - ) - }, - assert: func(t *testing.T, chain chains.Chain, btcConfig config.BTCConfig, enabled bool) { - assert.Equal(t, chains.BitcoinMainnet.ChainId, chain.ChainId) - assert.Equal(t, "smoketest", btcConfig.RPCUsername) - assert.True(t, enabled) - }, - }, - } { - t.Run(tt.name, func(t *testing.T) { - // ARRANGE - // Given app context - appContext := context.New(tt.cfg, logger) - - // And optional setup - if tt.setup != nil { - tt.setup(appContext) - } - - // ACT - chain, btcConfig, enabled := appContext.GetBTCChainAndConfig() - - // ASSERT - tt.assert(t, chain, btcConfig, enabled) - }) - } -} - -func TestGetBlockHeaderEnabledChains(t *testing.T) { - // ARRANGE - // Given app config - appContext := context.New(config.New(), zerolog.Nop()) - - // That was eventually updated - appContext.Update( - &observertypes.Keygen{}, - []chains.Chain{}, - nil, - &observertypes.ChainParams{ChainId: chains.BitcoinMainnet.ChainId}, - "", - observertypes.CrosschainFlags{}, - []chains.Chain{}, - []lightclienttypes.HeaderSupportedChain{ - {ChainId: 1, Enabled: true}, - }, - true, - ) - - // ACT #1 (found) - chain, found := appContext.GetBlockHeaderEnabledChains(1) - - // ASSERT #1 - assert.True(t, found) - assert.Equal(t, int64(1), chain.ChainId) - assert.True(t, chain.Enabled) - - // ACT #2 (not found) - chain, found = appContext.GetBlockHeaderEnabledChains(2) - - // ASSERT #2 - assert.False(t, found) - assert.Empty(t, chain) -} - -func TestGetAdditionalChains(t *testing.T) { - // ARRANGE - // Given app config - appContext := context.New(config.New(), zerolog.Nop()) - - additionalChains := []chains.Chain{ - sample.Chain(1), - sample.Chain(2), - sample.Chain(3), - } - - // That was eventually updated - appContext.Update( - &observertypes.Keygen{}, - []chains.Chain{}, - nil, - &observertypes.ChainParams{}, - "", - observertypes.CrosschainFlags{}, - additionalChains, - []lightclienttypes.HeaderSupportedChain{ - {ChainId: 1, Enabled: true}, - }, - true, - ) - - // ACT - found := appContext.GetAdditionalChains() - - // ASSERT - assert.EqualValues(t, additionalChains, found) -} - -func makeAppContext( - evmChain chains.Chain, - evmChainParams *observertypes.ChainParams, - ccFlags observertypes.CrosschainFlags, - headerSupportedChains []lightclienttypes.HeaderSupportedChain, -) *context.AppContext { - // create config - cfg := config.NewConfig() - logger := zerolog.Nop() - cfg.EVMChainConfigs[evmChain.ChainId] = config.EVMConfig{ - Chain: evmChain, - } - - // create zetacore context - coreContext := context.New(cfg, logger) - evmChainParamsMap := make(map[int64]*observertypes.ChainParams) - evmChainParamsMap[evmChain.ChainId] = evmChainParams - - // feed chain params - coreContext.Update( - &observertypes.Keygen{}, - []chains.Chain{evmChain}, - evmChainParamsMap, - nil, - "", - ccFlags, - []chains.Chain{}, - headerSupportedChains, - true, - ) - - return coreContext -} diff --git a/zetaclient/context/context.go b/zetaclient/context/context.go deleted file mode 100644 index 3f8b6177fa..0000000000 --- a/zetaclient/context/context.go +++ /dev/null @@ -1,26 +0,0 @@ -package context - -import ( - goctx "context" - - "github.com/pkg/errors" -) - -type appContextKey struct{} - -var ErrNotSet = errors.New("AppContext is not set in the context.Context") - -// WithAppContext applied AppContext to standard Go context.Context. -func WithAppContext(ctx goctx.Context, app *AppContext) goctx.Context { - return goctx.WithValue(ctx, appContextKey{}, app) -} - -// FromContext extracts AppContext from context.Context -func FromContext(ctx goctx.Context) (*AppContext, error) { - app, ok := ctx.Value(appContextKey{}).(*AppContext) - if !ok || app == nil { - return nil, ErrNotSet - } - - return app, nil -} diff --git a/zetaclient/context/context_test.go b/zetaclient/context/context_test.go deleted file mode 100644 index 5bde4596d6..0000000000 --- a/zetaclient/context/context_test.go +++ /dev/null @@ -1,40 +0,0 @@ -package context_test - -import ( - goctx "context" - "testing" - - "github.com/rs/zerolog" - "github.com/stretchr/testify/assert" - "github.com/zeta-chain/zetacore/zetaclient/config" - "github.com/zeta-chain/zetacore/zetaclient/context" -) - -func TestFromContext(t *testing.T) { - // ARRANGE #1 - // Given default go ctx - ctx := goctx.Background() - - // ACT #1 - // Extract App - _, err := context.FromContext(ctx) - - // ASSERT #1 - assert.ErrorIs(t, err, context.ErrNotSet) - - // ARRANGE #2 - // Given basic app - app := context.New(config.NewConfig(), zerolog.Nop()) - - // That is included in the ctx - ctx = context.WithAppContext(ctx, app) - - // ACT #2 - app2, err := context.FromContext(ctx) - - // ASSERT #2 - assert.NoError(t, err) - assert.NotNil(t, app2) - assert.Equal(t, app, app2) - assert.NotEmpty(t, app.Config()) -} diff --git a/zetaclient/orchestrator/app_context_update.go b/zetaclient/orchestrator/app_context_update.go new file mode 100644 index 0000000000..a8e9a57338 --- /dev/null +++ b/zetaclient/orchestrator/app_context_update.go @@ -0,0 +1,102 @@ +package orchestrator + +import ( + "fmt" + "time" + + "github.com/pkg/errors" + + "github.com/zeta-chain/zetacore/zetaclient/common" + "github.com/zeta-chain/zetacore/zetaclient/config" +) + +// WatchAppContext watches for app context changes and updates app context +func (oc *Orchestrator) WatchAppContext() { + oc.logger.Std.Info().Msg("UpdateAppContext started") + + ticker := time.NewTicker(time.Duration(oc.appContext.Config().ConfigUpdateTicker) * time.Second) + for { + select { + case <-ticker.C: + err := oc.UpdateAppContext() + if err != nil { + oc.logger.Std.Err(err).Msg("error updating zetaclient app context") + } + case <-oc.stop: + oc.logger.Std.Info().Msg("UpdateAppContext stopped") + return + } + } +} + +// WatchUpgradePlan watches for upgrade plan and stops orchestrator if upgrade height is reached +func (oc *Orchestrator) WatchUpgradePlan() { + oc.logger.Std.Info().Msg("WatchUpgradePlan started") + + // detect upgrade plan every half Zeta block in order to hit every height + ticker := time.NewTicker(common.ZetaBlockTime / 2) + for range ticker.C { + reached, err := oc.UpgradeHeightReached() + if err != nil { + oc.logger.Sampled.Error().Err(err).Msg("error detecting upgrade plan") + } else if reached { + oc.Stop() + oc.logger.Std.Info().Msg("WatchUpgradePlan stopped") + return + } + } +} + +// UpdateAppContext updates zetaclient app context +func (oc *Orchestrator) UpdateAppContext() error { + // fetch latest app context from zetacore + err := oc.zetacoreClient.UpdateAppContext(oc.appContext, oc.logger.Std) + if err != nil { + return errors.Wrap(err, "UpdateAppContext: error updating app context from zetacore") + } + + // reload config from file to allow for runtime config changes + // this allows operator to update zetaclient config without restarting zetaclient + zetazoreHome := oc.appContext.Config().ZetaCoreHome + newConfig, err := config.Load(zetazoreHome) + if err != nil { + return errors.Wrapf(err, "UpdateAppContext: error loading config from path %s", zetazoreHome) + } + + // set new config to app context + oc.appContext.SetConfig(newConfig) + + return nil +} + +// UpgradeHeightReached returns true if upgrade height is reached +func (oc *Orchestrator) UpgradeHeightReached() (bool, error) { + // query for active upgrade plan + plan, err := oc.zetacoreClient.GetUpgradePlan() + if err != nil { + return false, fmt.Errorf("failed to get upgrade plan: %w", err) + } + + // if there is no active upgrade plan, plan will be nil. + if plan == nil { + return false, nil + } + + // get ZetaChain block height + height, err := oc.zetacoreClient.GetBlockHeight() + if err != nil { + return false, fmt.Errorf("failed to get block height: %w", err) + } + + // if upgrade height is not reached, do nothing + if height != plan.Height-1 { + return false, nil + } + + // stop zetaclients if upgrade height is reached; notify operator to upgrade and restart + oc.logger.Std.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) + + return true, nil +} diff --git a/zetaclient/orchestrator/app_context_update_test.go b/zetaclient/orchestrator/app_context_update_test.go new file mode 100644 index 0000000000..ea0b6d926f --- /dev/null +++ b/zetaclient/orchestrator/app_context_update_test.go @@ -0,0 +1,162 @@ +package orchestrator_test + +import ( + "encoding/json" + "path" + "testing" + + upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" + "github.com/stretchr/testify/require" + "github.com/zeta-chain/zetacore/pkg/chains" + "github.com/zeta-chain/zetacore/testutil/sample" + "github.com/zeta-chain/zetacore/zetaclient/chains/base" + "github.com/zeta-chain/zetacore/zetaclient/config" + "github.com/zeta-chain/zetacore/zetaclient/orchestrator" + "github.com/zeta-chain/zetacore/zetaclient/testutils" + "github.com/zeta-chain/zetacore/zetaclient/testutils/mocks" +) + +// the relative path to the testdata directory +var TestDataDir = "../" + +func Test_UpdateAppContext(t *testing.T) { + // define test chains and chain params + evmChain := chains.Ethereum + btcChain := chains.BitcoinMainnet + evmChainParams := sample.ChainParams(evmChain.ChainId) + + // define test config + evmCfg := config.EVMConfig{ + Chain: evmChain, + Endpoint: "http://localhost:8545", + } + btcCfg := config.BTCConfig{ + RPCUsername: "user", + } + + // create app context + appCtx := createTestAppContext(evmCfg, btcCfg, evmChain, btcChain, evmChainParams, nil) + + t.Run("should update app context", func(t *testing.T) { + // create orchestrator + ztacoreClient := mocks.NewMockZetacoreClient() + oc := orchestrator.NewOrchestrator(appCtx, ztacoreClient, nil, base.Logger{}, testutils.SQLiteMemory, nil) + + // zetacoreHome directory + zetacoreHome := path.Join(TestDataDir, "testdata") + + // read archived config file and setup test zetacore home + cfg := testutils.LoadZetaclientConfig(t, TestDataDir) + cfg.ZetaCoreHome = zetacoreHome + + // set absolute path is needed to make the test pass + // config reloading will overwrite the relative path in config file + cfg.TssPath = config.GetPath(cfg.TssPath) + + // set zetacore home directory to the archived testdata directory + copyConfig := appCtx.Config() + copyConfig.ZetaCoreHome = zetacoreHome + appCtx.SetConfig(copyConfig) + + // update app context + err := oc.UpdateAppContext() + require.NoError(t, err) + + // get new config + newCfg := appCtx.Config() + + // serialize old and new config + oldCfgData, err := json.Marshal(cfg) + require.NoError(t, err) + + newCfgData, err := json.Marshal(newCfg) + require.NoError(t, err) + + // compare old and new config + require.JSONEq(t, string(oldCfgData), string(newCfgData)) + }) + t.Run("should return error if zetacore client fails to update app context", func(t *testing.T) { + // create orchestrator + ztacoreClient := mocks.NewMockZetacoreClient() + oc := orchestrator.NewOrchestrator(appCtx, ztacoreClient, nil, base.Logger{}, testutils.SQLiteMemory, nil) + + // pause zetacore client to simulate error + ztacoreClient.Pause() + + // update app context + err := oc.UpdateAppContext() + require.ErrorContains(t, err, "error updating app context") + }) + t.Run("should return error if reading config file fails", func(t *testing.T) { + // create orchestrator + ztacoreClient := mocks.NewMockZetacoreClient() + oc := orchestrator.NewOrchestrator(appCtx, ztacoreClient, nil, base.Logger{}, testutils.SQLiteMemory, nil) + + // set invalid zetacore home directory + copyConfig := appCtx.Config() + copyConfig.ZetaCoreHome = "/invalid/path" + appCtx.SetConfig(copyConfig) + + // update app context + err := oc.UpdateAppContext() + require.ErrorContains(t, err, "error loading config from path") + }) +} + +func Test_UpgradeHeightReached(t *testing.T) { + t.Run("should return true if upgrade height is reached", func(t *testing.T) { + // create orchestrator + zetacoreClient := mocks.NewMockZetacoreClient() + oc := orchestrator.NewOrchestrator(nil, zetacoreClient, nil, base.Logger{}, testutils.SQLiteMemory, nil) + + // set upgrade plan and current height + zetacoreClient.WithBlockHeight(99) + zetacoreClient.WithUpgradedPlan(&upgradetypes.Plan{ + Height: 100, + }) + + // check if upgrade height is reached + reached, err := oc.UpgradeHeightReached() + require.NoError(t, err) + require.True(t, reached) + }) + t.Run("should return error if failed to get upgrade plan", func(t *testing.T) { + // create orchestrator + zetacoreClient := mocks.NewMockZetacoreClient() + oc := orchestrator.NewOrchestrator(nil, zetacoreClient, nil, base.Logger{}, testutils.SQLiteMemory, nil) + + // pause zetacore client to simulate error + zetacoreClient.Pause() + + // check if upgrade height is reached + reached, err := oc.UpgradeHeightReached() + require.ErrorContains(t, err, "failed to get upgrade plan") + require.False(t, reached) + }) + t.Run("should return false if there is no active upgrade plan", func(t *testing.T) { + // create orchestrator + zetacoreClient := mocks.NewMockZetacoreClient() + oc := orchestrator.NewOrchestrator(nil, zetacoreClient, nil, base.Logger{}, testutils.SQLiteMemory, nil) + + // check if upgrade height is reached + reached, err := oc.UpgradeHeightReached() + require.NoError(t, err) + require.False(t, reached) + }) + t.Run("should return false if upgrade height is not reached", func(t *testing.T) { + // create orchestrator + zetacoreClient := mocks.NewMockZetacoreClient() + oc := orchestrator.NewOrchestrator(nil, zetacoreClient, nil, base.Logger{}, testutils.SQLiteMemory, nil) + + // set upgrade plan and current height + zetacoreClient.WithBlockHeight(98) + zetacoreClient.WithUpgradedPlan(&upgradetypes.Plan{ + Height: 100, + }) + + // check if upgrade height is reached + reached, err := oc.UpgradeHeightReached() + require.NoError(t, err) + require.False(t, reached) + }) +} diff --git a/zetaclient/orchestrator/chain_activate.go b/zetaclient/orchestrator/chain_activate.go new file mode 100644 index 0000000000..983b920f1e --- /dev/null +++ b/zetaclient/orchestrator/chain_activate.go @@ -0,0 +1,256 @@ +package orchestrator + +import ( + "fmt" + "time" + + ethcommon "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/pkg/errors" + + btcobserver "github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin/observer" + btcrpc "github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin/rpc" + btcsigner "github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin/signer" + evmobserver "github.com/zeta-chain/zetacore/zetaclient/chains/evm/observer" + evmsigner "github.com/zeta-chain/zetacore/zetaclient/chains/evm/signer" + "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" + "github.com/zeta-chain/zetacore/zetaclient/common" + "github.com/zeta-chain/zetacore/zetaclient/config" +) + +// WatchActivatedChains watches for run-time chain activation and deactivation +func (oc *Orchestrator) WatchActivatedChains() { + oc.logger.Std.Info().Msg("WatchChainActivation started") + + ticker := time.NewTicker(common.ZetaBlockTime * 2) + for { + select { + case <-ticker.C: + err := oc.UpdateActivatedChains() + if err != nil { + oc.logger.Sampled.Error().Err(err).Msg("UpdateActivatedChains failed") + } + case <-oc.stop: + oc.logger.Std.Info().Msg("WatchChainActivation stopped") + return + } + } +} + +// UpdateActivatedChains updates activated chains accordingly according to chain params and config file +// +// The chains to be activated: +// - chain params flag 'IsSupported' is true AND +// - chain is configured in config file +// +// The chains to be deactivated: +// - chain params flag 'IsSupported' is false OR +// - chain is not configured in config file +// +// Note: +// - zetaclient will reload config file periodically and update in-memory config accordingly. +// - As an tss signer, please make sure the config file is always well configured and not missing any chain +func (oc *Orchestrator) UpdateActivatedChains() error { + // create new signer and observer maps + // Note: the keys of the two maps are chain IDs and they are always exactly matched + newSignerMap := make(map[int64]interfaces.ChainSigner) + newObserverMap := make(map[int64]interfaces.ChainObserver) + + // create new signers and observers + err := oc.CreateSignerObserverEVM(newSignerMap, newObserverMap) + if err != nil { + return err + } + err = oc.CreateSignerObserverBTC(newSignerMap, newObserverMap) + if err != nil { + return err + } + + // activate newly supported chains and deactivate chains that are no longer supported + oc.DeactivateChains(newObserverMap) + oc.ActivateChains(newSignerMap, newObserverMap) + + return nil +} + +// DeactivateChains deactivates chains that are no longer supported +func (oc *Orchestrator) DeactivateChains( + newObserverMap map[int64]interfaces.ChainObserver, +) { + // loop through existing observer map to deactivate chains that are not in new observer map + oc.mu.Lock() + defer oc.mu.Unlock() + for chainID, observer := range oc.observerMap { + _, found := newObserverMap[chainID] + if !found { + oc.logger.Std.Info().Msgf("DeactivateChains: deactivating chain %d", chainID) + observer.Stop() + + // remove signer and observer from maps + delete(oc.signerMap, chainID) + delete(oc.observerMap, chainID) + oc.logger.Std.Info().Msgf("DeactivateChains: deactivated chain %d", chainID) + } + } +} + +// ActivateChains activates newly supported chains +func (oc *Orchestrator) ActivateChains( + newSignerMap map[int64]interfaces.ChainSigner, + newObserverMap map[int64]interfaces.ChainObserver, +) { + // loop through new observer map to activate chains that are not in existing observer map + for chainID, observer := range newObserverMap { + _, found := oc.observerMap[chainID] + if !found { + oc.logger.Std.Info().Msgf("ActivateChains: activating chain %d", chainID) + + // open database and load data + err := observer.LoadDB(oc.dbPath) + if err != nil { + oc.logger.Std.Error(). + Err(err). + Msgf("ActivateChains: error LoadDB for chain %d", chainID) + continue + } + observer.Start() + + // add signer and observer to maps + oc.mu.Lock() + oc.signerMap[chainID] = newSignerMap[chainID] + oc.observerMap[chainID] = observer + oc.mu.Unlock() + + oc.logger.Std.Info().Msgf("ActivateChains: activated chain %d", chainID) + } + } +} + +// CreateSignerObserverEVM creates signer and observer maps for all enabled EVM chains +func (oc *Orchestrator) CreateSignerObserverEVM( + resultSignerMap map[int64]interfaces.ChainSigner, + resultObserverMap map[int64]interfaces.ChainObserver, +) error { + // create EVM-chain signers + for _, evmConfig := range oc.appContext.Config().GetAllEVMConfigs() { + chainParams, found := oc.appContext.GetExternalChainParams(evmConfig.Chain.ChainId) + if !found { + oc.logger.Sampled.Error(). + Msgf("CreateObserversEVM: chain parameter not found for chain %d", evmConfig.Chain.ChainId) + continue + } + connectorAddress := ethcommon.HexToAddress(chainParams.ConnectorContractAddress) + erc20CustodyAddress := ethcommon.HexToAddress(chainParams.Erc20CustodyContractAddress) + + // create RPC client + evmClient, err := ethclient.Dial(evmConfig.Endpoint) + if err != nil { + return errors.Wrapf( + err, + "error dailing endpoint %s for chain %d", + evmConfig.Endpoint, + evmConfig.Chain.ChainId, + ) + } + + // create signer + signer, err := evmsigner.NewSigner( + evmConfig.Chain, + oc.appContext, + oc.tss, + oc.ts, + oc.logger.Base, + evmConfig.Endpoint, + config.GetConnectorABI(), + config.GetERC20CustodyABI(), + connectorAddress, + erc20CustodyAddress) + if err != nil { + return errors.Wrapf(err, "error NewSigner for chain %d", evmConfig.Chain.ChainId) + } + + // create observer + observer, err := evmobserver.NewObserver( + evmConfig, + evmClient, + *chainParams, + oc.appContext, + oc.zetacoreClient, + oc.tss, + oc.logger.Base, + oc.ts, + ) + if err != nil { + return errors.Wrapf(err, "error NewObserver for chain %d", evmConfig.Chain.ChainId) + } + + // add signer and observer to result maps + resultSignerMap[evmConfig.Chain.ChainId] = signer + resultObserverMap[evmConfig.Chain.ChainId] = observer + } + + return nil +} + +// CreateSignerObserverBTC creates signer and observer maps for all enabled BTC chains +func (oc *Orchestrator) CreateSignerObserverBTC( + resultSignerMap map[int64]interfaces.ChainSigner, + resultObserverMap map[int64]interfaces.ChainObserver, +) error { + // get enabled BTC chains and config + btcChains := oc.appContext.GetEnabledBTCChains() + btcConfig, found := oc.appContext.Config().GetBTCConfig() + + // currently only one single BTC chain is supported + if !found { + oc.logger.Sampled.Warn().Msg("CreateObserversBTC: BTC config not found") + return nil + } + if len(btcChains) != 1 { + return fmt.Errorf("want single BTC chain, got %d", len(btcChains)) + } + + // create BTC-chain signers and observers + // loop is used here in case we have multiple btc chains in the future + for _, btcChain := range btcChains { + chainParams, found := oc.appContext.GetExternalChainParams(btcChain.ChainId) + if !found { + oc.logger.Sampled.Error(). + Msgf("CreateObserversBTC: chain parameter not found for chain %d", btcChain.ChainId) + continue + } + + // create RPC client + btcClient, err := btcrpc.NewRPCClient(btcConfig) + if err != nil { + return errors.Wrapf(err, "error NewRPCClient for chain %d", btcChain.ChainId) + } + + // create signer + signer, err := btcsigner.NewSigner(btcChain, oc.appContext, oc.tss, oc.ts, oc.logger.Base, btcConfig) + if err != nil { + return errors.Wrapf(err, "error NewSigner for chain %d", btcChain.ChainId) + } + + // create observer + observer, err := btcobserver.NewObserver( + btcChain, + btcClient, + *chainParams, + oc.appContext, + oc.zetacoreClient, + oc.tss, + oc.logger.Base, + oc.ts, + ) + if err != nil { + return errors.Wrapf(err, "error NewObserver for chain %d", btcChain.ChainId) + } + + // add signer and observer to result maps + resultSignerMap[btcChain.ChainId] = signer + resultObserverMap[btcChain.ChainId] = observer + } + + return nil +} diff --git a/zetaclient/orchestrator/chain_activate_test.go b/zetaclient/orchestrator/chain_activate_test.go new file mode 100644 index 0000000000..95728fb6cd --- /dev/null +++ b/zetaclient/orchestrator/chain_activate_test.go @@ -0,0 +1,423 @@ +package orchestrator_test + +import ( + "testing" + + "github.com/btcsuite/btcd/chaincfg" + "github.com/rs/zerolog" + "github.com/stretchr/testify/require" + "github.com/zeta-chain/zetacore/pkg/chains" + "github.com/zeta-chain/zetacore/testutil/sample" + observertypes "github.com/zeta-chain/zetacore/x/observer/types" + "github.com/zeta-chain/zetacore/zetaclient/chains/base" + "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" + "github.com/zeta-chain/zetacore/zetaclient/config" + context "github.com/zeta-chain/zetacore/zetaclient/context" + "github.com/zeta-chain/zetacore/zetaclient/orchestrator" + "github.com/zeta-chain/zetacore/zetaclient/testutils" + "github.com/zeta-chain/zetacore/zetaclient/testutils/mocks" +) + +// createTestAppContext creates a test app context with provided chain params and flags +func createTestAppContext( + evmCfg config.EVMConfig, + btcCfg config.BTCConfig, + evmChain chains.Chain, + btcChain chains.Chain, + evmChainParams *observertypes.ChainParams, + btcChainParams *observertypes.ChainParams, +) *context.AppContext { + // create config + cfg := config.NewConfig() + cfg.EVMChainConfigs[evmChain.ChainId] = evmCfg + cfg.BitcoinConfig = btcCfg + + // chains enabled + chainsEnabled := []chains.Chain{evmChain} + + // create chain param map + chainParamMap := make(map[int64]*observertypes.ChainParams) + if evmChainParams != nil { + chainParamMap[evmChain.ChainId] = evmChainParams + } + if btcChainParams != nil { + chainParamMap[btcChain.ChainId] = btcChainParams + chainsEnabled = append(chainsEnabled, btcChain) + } + + // create app context + appContext := context.New(cfg) + + // create sample crosschain flags and header supported chains + ccFlags := sample.CrosschainFlags() + headerSupportedChains := sample.HeaderSupportedChains() + + // feed app context fields + appContext.Update( + observertypes.Keygen{}, + "testpubkey", + chainsEnabled, + chainParamMap, + &chaincfg.MainNetParams, + *ccFlags, + []chains.Chain{}, + headerSupportedChains, + zerolog.Logger{}, + ) + return appContext +} + +func Test_ActivateChains(t *testing.T) { + // define test chain and chain params + evmChain := chains.Ethereum + evmChainParams := sample.ChainParams(evmChain.ChainId) + + // test cases + tests := []struct { + name string + evmCfg config.EVMConfig + btcCfg config.BTCConfig + evmChain chains.Chain + btcChain chains.Chain + evmChainParams *observertypes.ChainParams + dbPath string + fail bool + }{ + { + name: "should activate newly supported chains that are not in existing observer map", + evmCfg: config.EVMConfig{ + Chain: evmChain, + Endpoint: "http://localhost:8545", + }, + btcCfg: config.BTCConfig{}, // btc chain is not needed for this test + evmChain: evmChain, + evmChainParams: evmChainParams, + dbPath: testutils.SQLiteMemory, + fail: false, + }, + { + name: "should not activate chain if dbPath is invalid", + evmCfg: config.EVMConfig{ + Chain: evmChain, + Endpoint: "http://localhost:8545", + }, + btcCfg: config.BTCConfig{}, // btc chain is not needed for this test + evmChain: evmChain, + evmChainParams: evmChainParams, + dbPath: "", // invalid db path + fail: true, + }, + } + + // run tests + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // create app context + appCtx := createTestAppContext(tt.evmCfg, tt.btcCfg, tt.evmChain, tt.btcChain, tt.evmChainParams, nil) + + // create orchestrator + ztacoreClient := mocks.NewMockZetacoreClient() + oc := orchestrator.NewOrchestrator(appCtx, ztacoreClient, nil, base.Logger{}, tt.dbPath, nil) + + // create new signer and observer maps + newSignerMap := make(map[int64]interfaces.ChainSigner) + newObserverMap := make(map[int64]interfaces.ChainObserver) + oc.CreateSignerObserverEVM(newSignerMap, newObserverMap) + + // open db and save last block number to db to avoid RPC call + ob := newObserverMap[tt.evmChain.ChainId] + require.NotNil(t, ob) + err := ob.OpenDB(testutils.SQLiteMemory, tt.evmCfg.Chain.ChainName.String()) + require.NoError(t, err) + + err = ob.SaveLastBlockScanned(100) + require.NoError(t, err) + + // activate chains + oc.ActivateChains(newSignerMap, newObserverMap) + + // assert signer/observer map + ob, err1 := oc.GetUpdatedChainObserver(tt.evmChain.ChainId) + signer, err2 := oc.GetUpdatedSigner(tt.evmChain.ChainId) + + if tt.fail { + require.Error(t, err1) + require.Error(t, err2) + require.Nil(t, ob) + require.Nil(t, signer) + } else { + require.NoError(t, err1) + require.NoError(t, err2) + require.NotNil(t, ob) + require.NotNil(t, signer) + } + }) + } +} + +func Test_DeactivateChains(t *testing.T) { + // define test chain and chain params + evmChain := chains.Ethereum + evmChainParams := sample.ChainParams(evmChain.ChainId) + + // test cases + tests := []struct { + name string + evmCfg config.EVMConfig + btcCfg config.BTCConfig + evmChain chains.Chain + btcChain chains.Chain + evmChainParams *observertypes.ChainParams + dbPath string + }{ + { + name: "should deactivate chains that are not in new observer map", + evmCfg: config.EVMConfig{ + Chain: evmChain, + Endpoint: "http://localhost:8545", + }, + btcCfg: config.BTCConfig{}, // btc chain is not needed for this test + evmChain: evmChain, + evmChainParams: evmChainParams, + dbPath: testutils.SQLiteMemory, + }, + } + + // run tests + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // create app context + appCtx := createTestAppContext(tt.evmCfg, tt.btcCfg, tt.evmChain, tt.btcChain, tt.evmChainParams, nil) + + // create orchestrator + ztacoreClient := mocks.NewMockZetacoreClient() + oc := orchestrator.NewOrchestrator(appCtx, ztacoreClient, nil, base.Logger{}, tt.dbPath, nil) + + // create new signer and observer maps + newSignerMap := make(map[int64]interfaces.ChainSigner) + newObserverMap := make(map[int64]interfaces.ChainObserver) + oc.CreateSignerObserverEVM(newSignerMap, newObserverMap) + + // open db and save last block number to db to avoid RPC call + ob := newObserverMap[tt.evmChain.ChainId] + require.NotNil(t, ob) + err := ob.OpenDB(testutils.SQLiteMemory, tt.evmCfg.Chain.ChainName.String()) + require.NoError(t, err) + + err = ob.SaveLastBlockScanned(100) + require.NoError(t, err) + + // activate chains + oc.ActivateChains(newSignerMap, newObserverMap) + + // assert signer/observer map + ob, err = oc.GetUpdatedChainObserver(tt.evmChain.ChainId) + require.NoError(t, err) + require.NotNil(t, ob) + + // create new config and set EVM chain params as empty + newCfg := appCtx.Config() + newCfg.EVMChainConfigs = make(map[int64]config.EVMConfig) + appCtx.SetConfig(newCfg) + + // create maps again based on newly updated config + newSignerMap = make(map[int64]interfaces.ChainSigner) + newObserverMap = make(map[int64]interfaces.ChainObserver) + oc.CreateSignerObserverEVM(newSignerMap, newObserverMap) + + // deactivate chains + oc.DeactivateChains(newObserverMap) + + // assert signer/observer map + ob, err1 := oc.GetUpdatedChainObserver(tt.evmChain.ChainId) + signer, err2 := oc.GetUpdatedSigner(tt.evmChain.ChainId) + require.Error(t, err1) + require.Error(t, err2) + require.Nil(t, ob) + require.Nil(t, signer) + }) + } +} + +func Test_CreateSignerObserverEVM(t *testing.T) { + // define test chains and chain params + evmChain := chains.Ethereum + btcChain := chains.BitcoinMainnet + evmChainParams := sample.ChainParams(evmChain.ChainId) + + // test cases + tests := []struct { + name string + evmCfg config.EVMConfig + btcCfg config.BTCConfig + evmChain chains.Chain + btcChain chains.Chain + evmChainParams *observertypes.ChainParams + dbPath string + numObserverCreated int + fail bool + message string + }{ + { + name: "should create observers for EVM chain", + evmCfg: config.EVMConfig{ + Chain: evmChain, + Endpoint: "http://localhost:8545", + }, + btcCfg: config.BTCConfig{}, + evmChain: evmChain, + btcChain: btcChain, + evmChainParams: evmChainParams, + dbPath: testutils.SQLiteMemory, + numObserverCreated: 1, + fail: false, + }, + { + name: "should not create observer for EVM chain if chain params not found", + evmCfg: config.EVMConfig{ + Chain: evmChain, + Endpoint: "http://localhost:8545", + }, + btcCfg: config.BTCConfig{}, + evmChain: evmChain, + btcChain: btcChain, + evmChainParams: nil, + dbPath: testutils.SQLiteMemory, + numObserverCreated: 0, + fail: false, + }, + { + name: "should fail if endpoint is invalid", + evmCfg: config.EVMConfig{ + Chain: evmChain, + Endpoint: "invalid_endpoint", + }, + btcCfg: config.BTCConfig{}, + evmChain: evmChain, + btcChain: btcChain, + evmChainParams: evmChainParams, + dbPath: testutils.SQLiteMemory, + numObserverCreated: 0, + fail: true, + message: "error dailing endpoint", + }, + } + + // run tests + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // create app context + appCtx := createTestAppContext(tt.evmCfg, tt.btcCfg, tt.evmChain, tt.btcChain, tt.evmChainParams, nil) + + // create orchestrator + ztacoreClient := mocks.NewMockZetacoreClient() + oc := orchestrator.NewOrchestrator(appCtx, ztacoreClient, nil, base.Logger{}, tt.dbPath, nil) + + // create observers + signerMap := make(map[int64]interfaces.ChainSigner) + observerMap := make(map[int64]interfaces.ChainObserver) + err := oc.CreateSignerObserverEVM(signerMap, observerMap) + + // assert error if it should fail + if tt.fail { + require.ErrorContains(t, err, tt.message) + } + + // assert signer/observer map + require.Len(t, signerMap, tt.numObserverCreated) + require.Len(t, observerMap, tt.numObserverCreated) + + // assert signer/observer chain ID + if tt.numObserverCreated > 0 { + require.NotNil(t, signerMap[evmChain.ChainId]) + require.NotNil(t, observerMap[evmChain.ChainId]) + } + }) + } +} + +func Test_CreateSignerObserverBTC(t *testing.T) { + // define test chains and chain params + evmChain := chains.Ethereum + btcChain := chains.BitcoinMainnet + btcChainParams := sample.ChainParams(btcChain.ChainId) + + // test cases + tests := []struct { + name string + evmCfg config.EVMConfig + btcCfg config.BTCConfig + evmChain chains.Chain + btcChain chains.Chain + btcChainParams *observertypes.ChainParams + dbPath string + numObserverCreated int + fail bool + message string + }{ + { + name: "should not create observer for BTC chain if btc config is missing", + evmCfg: config.EVMConfig{}, + btcCfg: config.BTCConfig{}, // empty config in file + evmChain: evmChain, + btcChain: btcChain, + btcChainParams: btcChainParams, + dbPath: testutils.SQLiteMemory, + numObserverCreated: 0, + }, + { + name: "should fail if number of BTC chain is not 1", + evmCfg: config.EVMConfig{}, + btcCfg: config.BTCConfig{ + RPCUsername: "user", + }, + evmChain: evmChain, + btcChain: btcChain, + btcChainParams: nil, // disabled btc chain + dbPath: testutils.SQLiteMemory, + numObserverCreated: 0, + fail: true, + message: "want single BTC chain, got 0", + }, + { + name: "should fail if unable to create rpc client", + evmCfg: config.EVMConfig{}, + btcCfg: config.BTCConfig{ + RPCUsername: "user", + }, + evmChain: evmChain, + btcChain: btcChain, + btcChainParams: btcChainParams, + dbPath: testutils.SQLiteMemory, + numObserverCreated: 0, + fail: true, + message: "error NewRPCClient", + }, + } + + // run tests + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // create app context + appCtx := createTestAppContext(tt.evmCfg, tt.btcCfg, tt.evmChain, tt.btcChain, nil, tt.btcChainParams) + + // create orchestrator + ztacoreClient := mocks.NewMockZetacoreClient() + oc := orchestrator.NewOrchestrator(appCtx, ztacoreClient, nil, base.Logger{}, tt.dbPath, nil) + + // create observers + signerMap := make(map[int64]interfaces.ChainSigner) + observerMap := make(map[int64]interfaces.ChainObserver) + err := oc.CreateSignerObserverBTC(signerMap, observerMap) + + // assert error if it should fail + if tt.fail { + require.ErrorContains(t, err, tt.message) + } + + // assert signer/observer map + require.Len(t, signerMap, tt.numObserverCreated) + require.Len(t, observerMap, tt.numObserverCreated) + }) + } +} diff --git a/zetaclient/orchestrator/orchestrator.go b/zetaclient/orchestrator/orchestrator.go index b233c9d582..89ce7f9400 100644 --- a/zetaclient/orchestrator/orchestrator.go +++ b/zetaclient/orchestrator/orchestrator.go @@ -4,6 +4,10 @@ package orchestrator import ( "fmt" "math" + "os" + "os/signal" + "sync" + "syscall" "time" sdkmath "cosmossdk.io/math" @@ -14,6 +18,7 @@ import ( 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" + "github.com/zeta-chain/zetacore/zetaclient/chains/base" 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" @@ -36,15 +41,27 @@ const ( // Log is a struct that contains the logger // TODO(revamp): rename to logger type Log struct { - Std zerolog.Logger + // Base are the original base loggers used by orchestrator to create observers + Base base.Logger + + // Std is the standard logger for orchestrator module + Std zerolog.Logger + + // Sampled is the sampled logger for orchestrator module Sampled zerolog.Logger } // Orchestrator wraps the zetacore client, chain observers and signers. This is the high level object used for CCTX scheduling type Orchestrator struct { + // appContext contains the zetaclient application context + appContext *context.AppContext + // zetacore client zetacoreClient interfaces.ZetacoreClient + // tss is the TSS signer + tss interfaces.TSSSigner + // signerMap contains the chain signers indexed by chainID signerMap map[int64]interfaces.ChainSigner @@ -57,40 +74,60 @@ type Orchestrator struct { // last operator balance lastOperatorBalance sdkmath.Int - // misc + // logger contains the loggers used by the orchestrator logger Log - stop chan struct{} - ts *metrics.TelemetryServer + + // dbPath is the path observer database + dbPath string + + // ts is the telemetry server for metrics + ts *metrics.TelemetryServer + + // mu protects fields from concurrent access + mu sync.Mutex + + // stop channel + stop chan struct{} + + // stop flag to used to prevent the 'stop' channel from being closed multiple times (panic). + // the 'stop' channel will be closed either by system signals or by the 'WatchUpgradePlan' goroutine + stopped bool } // NewOrchestrator creates a new orchestrator func NewOrchestrator( + appContext *context.AppContext, zetacoreClient interfaces.ZetacoreClient, - signerMap map[int64]interfaces.ChainSigner, - observerMap map[int64]interfaces.ChainObserver, - logger zerolog.Logger, + tss interfaces.TSSSigner, + logger base.Logger, + dbPath string, ts *metrics.TelemetryServer, ) *Orchestrator { oc := Orchestrator{ - ts: ts, - stop: make(chan struct{}), + appContext: appContext, + zetacoreClient: zetacoreClient, + tss: tss, + signerMap: make(map[int64]interfaces.ChainSigner), + observerMap: make(map[int64]interfaces.ChainObserver), + dbPath: dbPath, + ts: ts, + mu: sync.Mutex{}, + stop: make(chan struct{}), + stopped: false, } // create loggers oc.logger = Log{ - Std: logger.With().Str("module", "Orchestrator").Logger(), + Base: logger, + Std: logger.Std.With().Str("module", "orchestrator").Logger(), } oc.logger.Sampled = oc.logger.Std.Sample(&zerolog.BasicSampler{N: loggerSamplingRate}) - // set zetacore client, signers and chain observers - oc.zetacoreClient = zetacoreClient - oc.signerMap = signerMap - oc.observerMap = observerMap - // create outbound processor - oc.outboundProc = outboundprocessor.NewProcessor(logger) + oc.outboundProc = outboundprocessor.NewProcessor(logger.Std) - balance, err := zetacoreClient.GetZetaHotKeyBalance() + // initialize hot key balance + balance, err := oc.zetacoreClient.GetZetaHotKeyBalance() if err != nil { oc.logger.Std.Error().Err(err).Msg("error getting last balance of the hot key") } @@ -99,89 +136,104 @@ func NewOrchestrator( return &oc } -// MonitorCore starts the orchestrator for CCTXs -func (oc *Orchestrator) MonitorCore(appContext *context.AppContext) error { - signerAddress, err := oc.zetacoreClient.GetKeys().GetAddress() - if err != nil { - return fmt.Errorf("failed to get signer address: %w", err) - } - oc.logger.Std.Info().Msgf("Starting orchestrator for signer: %s", signerAddress) +// Start all orchestrator routines +func (oc *Orchestrator) Start() { + // watch for zetaclient app context changes + go oc.WatchAppContext() + + // watch for upgrade plan in zetacore + go oc.WatchUpgradePlan() + + // watch for chain activation/deactivation + go oc.WatchActivatedChains() + + // schedule pending cctxs across all enabled chains + go oc.SchedulePendingCctxs() + + // watch for stop signals + oc.AwaitStopSignals() +} + +// AwaitStopSignals waits for stop signals +func (oc *Orchestrator) AwaitStopSignals() { + oc.logger.Std.Info().Msgf("Orchestrator awaiting the os.Interrupt, syscall.SIGTERM signals...") + + // subscribe to stop signals + ch := make(chan os.Signal, 1) + signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM) + sig := <-ch - // start cctx scheduler - go oc.StartCctxScheduler(appContext) + // stop orchestrator + oc.Stop() + oc.logger.Std.Info().Msgf("Orchestrator stopped on signal: %s", sig) +} - // watch for upgrade plan from zetacore - go func() { - // wait for upgrade plan signal to arrive - oc.zetacoreClient.Pause() +// Stop notifies all zetaclient goroutines to stop +func (oc *Orchestrator) Stop() { + oc.mu.Lock() + defer oc.mu.Unlock() - // now stop orchestrator and all observers + // stop orchestrator only once. + // both WatchUpgradePlan and system signals can trigger Stop() + if !oc.stopped { + // notify app context updater and CCTX scheduler to stop close(oc.stop) + + // notify all chain observers to stop for _, c := range oc.observerMap { c.Stop() } - }() - - return nil + // set stopped flag + oc.stopped = true + } } // GetUpdatedSigner returns signer with updated chain parameters -func (oc *Orchestrator) GetUpdatedSigner( - appContext *context.AppContext, - chainID int64, -) (interfaces.ChainSigner, error) { +func (oc *Orchestrator) GetUpdatedSigner(chainID int64) (interfaces.ChainSigner, error) { + oc.mu.Lock() signer, found := oc.signerMap[chainID] + oc.mu.Unlock() if !found { return nil, fmt.Errorf("signer not found for chainID %d", chainID) } - // update EVM signer parameters only. BTC signer doesn't use chain parameters for now. - if chains.IsEVMChain(chainID, appContext.GetAdditionalChains()) { - evmParams, found := appContext.GetEVMChainParams(chainID) - if found { - // update zeta connector and ERC20 custody addresses - zetaConnectorAddress := ethcommon.HexToAddress(evmParams.GetConnectorContractAddress()) - erc20CustodyAddress := ethcommon.HexToAddress(evmParams.GetErc20CustodyContractAddress()) - if zetaConnectorAddress != signer.GetZetaConnectorAddress() { - signer.SetZetaConnectorAddress(zetaConnectorAddress) - oc.logger.Std.Info().Msgf( - "updated zeta connector address for chainID %d, new address: %s", chainID, zetaConnectorAddress) - } - if erc20CustodyAddress != signer.GetERC20CustodyAddress() { - signer.SetERC20CustodyAddress(erc20CustodyAddress) - oc.logger.Std.Info().Msgf( - "updated ERC20 custody address for chainID %d, new address: %s", chainID, erc20CustodyAddress) - } + + // update signer parameters for the chain. + // the logic is consistent for all chains, even if BTC chain doesn't have zetaConnector/erc20Custody. + evmParams, found := oc.appContext.GetExternalChainParams(chainID) + if found { + // update zeta connector and ERC20 custody addresses + zetaConnectorAddress := ethcommon.HexToAddress(evmParams.GetConnectorContractAddress()) + erc20CustodyAddress := ethcommon.HexToAddress(evmParams.GetErc20CustodyContractAddress()) + if zetaConnectorAddress != signer.GetZetaConnectorAddress() { + signer.SetZetaConnectorAddress(zetaConnectorAddress) + oc.logger.Std.Info().Msgf( + "updated zeta connector address for chainID %d, new address: %s", chainID, zetaConnectorAddress) + } + if erc20CustodyAddress != signer.GetERC20CustodyAddress() { + signer.SetERC20CustodyAddress(erc20CustodyAddress) + oc.logger.Std.Info().Msgf( + "updated ERC20 custody address for chainID %d, new address: %s", chainID, erc20CustodyAddress) } } return signer, nil } // GetUpdatedChainObserver returns chain observer with updated chain parameters -func (oc *Orchestrator) GetUpdatedChainObserver( - appContext *context.AppContext, - chainID int64, -) (interfaces.ChainObserver, error) { +func (oc *Orchestrator) GetUpdatedChainObserver(chainID int64) (interfaces.ChainObserver, error) { + oc.mu.Lock() observer, found := oc.observerMap[chainID] + oc.mu.Unlock() if !found { return nil, fmt.Errorf("chain observer not found for chainID %d", chainID) } - // update chain observer chain parameters - curParams := observer.GetChainParams() - if chains.IsEVMChain(chainID, appContext.GetAdditionalChains()) { - evmParams, found := appContext.GetEVMChainParams(chainID) - if found && !observertypes.ChainParamsEqual(curParams, *evmParams) { - observer.SetChainParams(*evmParams) - oc.logger.Std.Info().Msgf( - "updated chain params for chainID %d, new params: %v", chainID, *evmParams) - } - } else if chains.IsBitcoinChain(chainID, appContext.GetAdditionalChains()) { - _, btcParams, found := appContext.GetBTCChainParams() - if found && !observertypes.ChainParamsEqual(curParams, *btcParams) { - observer.SetChainParams(*btcParams) - oc.logger.Std.Info().Msgf( - "updated chain params for Bitcoin, new params: %v", *btcParams) - } + // update chain observer chain parameters + oldParams := observer.GetChainParams() + newParams, found := oc.appContext.GetExternalChainParams(chainID) + if found && !observertypes.ChainParamsEqual(oldParams, *newParams) { + observer.SetChainParams(*newParams) + oc.logger.Std.Info().Msgf( + "updated chain params for chainID %d, new params: %v", chainID, *newParams) } return observer, nil } @@ -236,9 +288,9 @@ func (oc *Orchestrator) GetPendingCctxsWithinRatelimit( return output.CctxsMap, nil } -// StartCctxScheduler schedules keysigns for cctxs on each ZetaChain block (the ticker) +// SchedulePendingCctxs schedules keysigns for pending cctxs across all chains on ticker // TODO(revamp): make this function simpler -func (oc *Orchestrator) StartCctxScheduler(appContext *context.AppContext) { +func (oc *Orchestrator) SchedulePendingCctxs() { observeTicker := time.NewTicker(3 * time.Second) var lastBlockNum int64 for { @@ -281,7 +333,7 @@ func (oc *Orchestrator) StartCctxScheduler(appContext *context.AppContext) { metrics.HotKeyBurnRate.Set(float64(oc.ts.HotKeyBurnRate.GetBurnRate().Int64())) // get supported external chains - externalChains := appContext.GetEnabledExternalChains() + externalChains := oc.appContext.GetEnabledExternalChains() // query pending cctxs across all external chains within rate limit cctxMap, err := oc.GetPendingCctxsWithinRatelimit(externalChains) @@ -299,29 +351,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(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(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 !oc.appContext.IsOutboundObservationEnabled(ob.GetChainParams()) { continue } // #nosec G701 range is verified zetaHeight := uint64(bn) - if chains.IsEVMChain(c.ChainId, appContext.GetAdditionalChains()) { + additionalChains := oc.appContext.GetAdditionalChains() + if chains.IsEVMChain(c.ChainId, additionalChains) { oc.ScheduleCctxEVM(zetaHeight, c.ChainId, cctxList, ob, signer) - } else if chains.IsBitcoinChain(c.ChainId, appContext.GetAdditionalChains()) { + } else if chains.IsBitcoinChain(c.ChainId, additionalChains) { oc.ScheduleCctxBTC(zetaHeight, c.ChainId, cctxList, ob, signer) } else { oc.logger.Std.Error().Msgf("StartCctxScheduler: unsupported chain %d", c.ChainId) diff --git a/zetaclient/orchestrator/orchestrator_test.go b/zetaclient/orchestrator/orchestrator_test.go index 9eab334bfe..9b7ab147ca 100644 --- a/zetaclient/orchestrator/orchestrator_test.go +++ b/zetaclient/orchestrator/orchestrator_test.go @@ -3,6 +3,7 @@ package orchestrator import ( "testing" + "github.com/btcsuite/btcd/chaincfg" sdk "github.com/cosmos/cosmos-sdk/types" ethcommon "github.com/ethereum/go-ethereum/common" "github.com/rs/zerolog" @@ -24,6 +25,7 @@ import ( // MockOrchestrator creates a mock orchestrator for testing func MockOrchestrator( t *testing.T, + appContext *context.AppContext, zetacoreClient interfaces.ZetacoreClient, evmChain, btcChain chains.Chain, evmChainParams, btcChainParams *observertypes.ChainParams, @@ -40,6 +42,7 @@ func MockOrchestrator( // create orchestrator orchestrator := &Orchestrator{ + appContext: appContext, zetacoreClient: zetacoreClient, signerMap: map[int64]interfaces.ChainSigner{ evmChain.ChainId: evmSigner, @@ -49,11 +52,14 @@ func MockOrchestrator( evmChain.ChainId: evmObserver, btcChain.ChainId: btcObserver, }, + stop: make(chan struct{}), + stopped: false, } return orchestrator } -func CreateAppContext( +// CreateTestAppContext creates a test app context for orchestrator testing +func CreateTestAppContext( evmChain, btcChain chains.Chain, evmChainParams, btcChainParams *observertypes.ChainParams, ) *context.AppContext { @@ -65,28 +71,51 @@ func CreateAppContext( cfg.BitcoinConfig = config.BTCConfig{ RPCHost: "localhost", } - // new zetacore context - appContext := context.New(cfg, zerolog.Nop()) - evmChainParamsMap := make(map[int64]*observertypes.ChainParams) - evmChainParamsMap[evmChain.ChainId] = evmChainParams + // new app context + appContext := context.New(cfg) + chainParamsMap := make(map[int64]*observertypes.ChainParams) + chainParamsMap[evmChain.ChainId] = evmChainParams + chainParamsMap[btcChain.ChainId] = btcChainParams ccFlags := sample.CrosschainFlags() verificationFlags := sample.HeaderSupportedChains() // feed chain params appContext.Update( - &observertypes.Keygen{}, - []chains.Chain{evmChain, btcChain}, - evmChainParamsMap, - btcChainParams, + observertypes.Keygen{}, "", + []chains.Chain{evmChain, btcChain}, + chainParamsMap, + &chaincfg.RegressionNetParams, *ccFlags, []chains.Chain{}, verificationFlags, - true, + zerolog.Logger{}, ) return appContext } +func Test_Stop(t *testing.T) { + // initial parameters for orchestrator creation + evmChain := chains.Ethereum + btcChain := chains.BitcoinMainnet + evmChainParams := &observertypes.ChainParams{ + ChainId: evmChain.ChainId, + } + btcChainParams := &observertypes.ChainParams{ + ChainId: btcChain.ChainId, + } + + // create orchestrator + appCtx := CreateTestAppContext(evmChain, btcChain, evmChainParams, btcChainParams) + orchestrator := MockOrchestrator(t, appCtx, nil, evmChain, btcChain, evmChainParams, btcChainParams) + + // stop orchestrator 1st time + orchestrator.Stop() + + // stop orchestrator 2nd time without panic + orchestrator.Stop() +} + func Test_GetUpdatedSigner(t *testing.T) { // initial parameters for orchestrator creation evmChain := chains.Ethereum @@ -98,7 +127,7 @@ func Test_GetUpdatedSigner(t *testing.T) { } btcChainParams := &observertypes.ChainParams{} - // new chain params in zetacore context + // new chain params in app context evmChainParamsNew := &observertypes.ChainParams{ ChainId: evmChain.ChainId, ConnectorContractAddress: testutils.OtherAddress1, @@ -106,17 +135,18 @@ func Test_GetUpdatedSigner(t *testing.T) { } t.Run("signer should not be found", func(t *testing.T) { - orchestrator := MockOrchestrator(t, nil, evmChain, btcChain, evmChainParams, btcChainParams) - context := CreateAppContext(evmChain, btcChain, evmChainParamsNew, btcChainParams) + appCtx := CreateTestAppContext(evmChain, btcChain, evmChainParamsNew, btcChainParams) + orchestrator := MockOrchestrator(t, appCtx, nil, evmChain, btcChain, evmChainParams, btcChainParams) // BSC signer should not be found - _, err := orchestrator.GetUpdatedSigner(context, chains.BscMainnet.ChainId) + _, err := orchestrator.GetUpdatedSigner(chains.BscMainnet.ChainId) require.ErrorContains(t, err, "signer not found") }) t.Run("should be able to update connector and erc20 custody address", func(t *testing.T) { - orchestrator := MockOrchestrator(t, nil, evmChain, btcChain, evmChainParams, btcChainParams) - context := CreateAppContext(evmChain, btcChain, evmChainParamsNew, btcChainParams) + appCtx := CreateTestAppContext(evmChain, btcChain, evmChainParamsNew, btcChainParams) + orchestrator := MockOrchestrator(t, appCtx, nil, evmChain, btcChain, evmChainParams, btcChainParams) + // update signer with new connector and erc20 custody address - signer, err := orchestrator.GetUpdatedSigner(context, evmChain.ChainId) + signer, err := orchestrator.GetUpdatedSigner(evmChain.ChainId) require.NoError(t, err) require.Equal(t, testutils.OtherAddress1, signer.GetZetaConnectorAddress().Hex()) require.Equal(t, testutils.OtherAddress2, signer.GetERC20CustodyAddress().Hex()) @@ -136,7 +166,7 @@ func Test_GetUpdatedChainObserver(t *testing.T) { ChainId: btcChain.ChainId, } - // new chain params in zetacore context + // new chain params in app context evmChainParamsNew := &observertypes.ChainParams{ ChainId: evmChain.ChainId, ConfirmationCount: 10, @@ -171,33 +201,37 @@ func Test_GetUpdatedChainObserver(t *testing.T) { } t.Run("evm chain observer should not be found", func(t *testing.T) { - orchestrator := MockOrchestrator(t, nil, evmChain, btcChain, evmChainParams, btcChainParams) - coreContext := CreateAppContext(evmChain, btcChain, evmChainParamsNew, btcChainParams) + appCtx := CreateTestAppContext(evmChain, btcChain, evmChainParamsNew, btcChainParams) + orchestrator := MockOrchestrator(t, appCtx, nil, evmChain, btcChain, evmChainParams, btcChainParams) + // BSC chain observer should not be found - _, err := orchestrator.GetUpdatedChainObserver(coreContext, chains.BscMainnet.ChainId) + _, err := orchestrator.GetUpdatedChainObserver(chains.BscMainnet.ChainId) require.ErrorContains(t, err, "chain observer not found") }) t.Run("chain params in evm chain observer should be updated successfully", func(t *testing.T) { - orchestrator := MockOrchestrator(t, nil, evmChain, btcChain, evmChainParams, btcChainParams) - coreContext := CreateAppContext(evmChain, btcChain, evmChainParamsNew, btcChainParams) + appCtx := CreateTestAppContext(evmChain, btcChain, evmChainParamsNew, btcChainParams) + orchestrator := MockOrchestrator(t, appCtx, nil, evmChain, btcChain, evmChainParams, btcChainParams) + // update evm chain observer with new chain params - chainOb, err := orchestrator.GetUpdatedChainObserver(coreContext, evmChain.ChainId) + chainOb, err := orchestrator.GetUpdatedChainObserver(evmChain.ChainId) require.NoError(t, err) require.NotNil(t, chainOb) require.True(t, observertypes.ChainParamsEqual(*evmChainParamsNew, chainOb.GetChainParams())) }) t.Run("btc chain observer should not be found", func(t *testing.T) { - orchestrator := MockOrchestrator(t, nil, evmChain, btcChain, evmChainParams, btcChainParams) - coreContext := CreateAppContext(btcChain, btcChain, evmChainParams, btcChainParamsNew) + appCtx := CreateTestAppContext(btcChain, btcChain, evmChainParams, btcChainParamsNew) + orchestrator := MockOrchestrator(t, appCtx, nil, evmChain, btcChain, evmChainParams, btcChainParams) + // BTC testnet chain observer should not be found - _, err := orchestrator.GetUpdatedChainObserver(coreContext, chains.BitcoinTestnet.ChainId) + _, err := orchestrator.GetUpdatedChainObserver(chains.BitcoinTestnet.ChainId) require.ErrorContains(t, err, "chain observer not found") }) t.Run("chain params in btc chain observer should be updated successfully", func(t *testing.T) { - orchestrator := MockOrchestrator(t, nil, evmChain, btcChain, evmChainParams, btcChainParams) - coreContext := CreateAppContext(btcChain, btcChain, evmChainParams, btcChainParamsNew) + appCtx := CreateTestAppContext(btcChain, btcChain, evmChainParams, btcChainParamsNew) + orchestrator := MockOrchestrator(t, appCtx, nil, evmChain, btcChain, evmChainParams, btcChainParams) + // update btc chain observer with new chain params - chainOb, err := orchestrator.GetUpdatedChainObserver(coreContext, btcChain.ChainId) + chainOb, err := orchestrator.GetUpdatedChainObserver(btcChain.ChainId) require.NoError(t, err) require.NotNil(t, chainOb) require.True(t, observertypes.ChainParamsEqual(*btcChainParamsNew, chainOb.GetChainParams())) @@ -362,7 +396,7 @@ func Test_GetPendingCctxsWithinRatelimit(t *testing.T) { client.WithRateLimiterInput(tt.response) // create orchestrator - orchestrator := MockOrchestrator(t, client, ethChain, btcChain, ethChainParams, btcChainParams) + orchestrator := MockOrchestrator(t, nil, client, ethChain, btcChain, ethChainParams, btcChainParams) // run the test cctxsMap, err := orchestrator.GetPendingCctxsWithinRatelimit(foreignChains) diff --git a/zetaclient/supplychecker/zeta_supply_checker.go b/zetaclient/supplychecker/zeta_supply_checker.go index 3355d4ff5e..c119ff50a4 100644 --- a/zetaclient/supplychecker/zeta_supply_checker.go +++ b/zetaclient/supplychecker/zeta_supply_checker.go @@ -123,12 +123,12 @@ func (zs *ZetaSupplyChecker) Stop() { func (zs *ZetaSupplyChecker) CheckZetaTokenSupply() error { externalChainTotalSupply := sdkmath.ZeroInt() for _, chain := range zs.externalEvmChain { - externalEvmChainParams, ok := zs.appContext.GetEVMChainParams(chain.ChainId) + evmChainParams, ok := zs.appContext.GetExternalChainParams(chain.ChainId) if !ok { return fmt.Errorf("externalEvmChainParams not found for chain id %d", chain.ChainId) } - zetaTokenAddressString := externalEvmChainParams.ZetaTokenContractAddress + zetaTokenAddressString := evmChainParams.ZetaTokenContractAddress zetaTokenAddress := ethcommon.HexToAddress(zetaTokenAddressString) zetatokenNonEth, err := observer.FetchZetaTokenContract(zetaTokenAddress, zs.evmClient[chain.ChainId]) if err != nil { @@ -149,12 +149,12 @@ func (zs *ZetaSupplyChecker) CheckZetaTokenSupply() error { externalChainTotalSupply = externalChainTotalSupply.Add(totalSupplyInt) } - evmChainParams, ok := zs.appContext.GetEVMChainParams(zs.ethereumChain.ChainId) + ethChainParams, ok := zs.appContext.GetExternalChainParams(zs.ethereumChain.ChainId) if !ok { return fmt.Errorf("eth config not found for chain id %d", zs.ethereumChain.ChainId) } - ethConnectorAddressString := evmChainParams.ConnectorContractAddress + ethConnectorAddressString := ethChainParams.ConnectorContractAddress ethConnectorAddress := ethcommon.HexToAddress(ethConnectorAddressString) ethConnectorContract, err := observer.FetchConnectorContractEth( ethConnectorAddress, diff --git a/zetaclient/testdata/config/zetaclient_config.json b/zetaclient/testdata/config/zetaclient_config.json new file mode 100644 index 0000000000..4f99002d96 --- /dev/null +++ b/zetaclient/testdata/config/zetaclient_config.json @@ -0,0 +1,114 @@ +{ + "Peer": "", + "PublicIP": "172.20.0.21", + "LogFormat": "text", + "LogLevel": 1, + "LogSampler": false, + "PreParamsPath": "/root/preparams/zetaclient0.json", + "ZetaCoreHome": "", + "ChainID": "athens_101-1", + "ZetaCoreURL": "zetacore0", + "AuthzGranter": "zeta1se2xay3sqeml2d8s3lumlgkkjmu4nsev0qyzaw", + "AuthzHotkey": "hotkey", + "P2PDiagnostic": false, + "ConfigUpdateTicker": 5, + "P2PDiagnosticTicker": 30, + "TssPath": "~/.tss", + "TestTssKeysign": false, + "KeyringBackend": "file", + "HsmMode": false, + "HsmHotKey": "hsm-hotkey", + "EVMChainConfigs": { + "1": { + "Chain": { + "chain_id": 1, + "chain_name": 1, + "vm": 1, + "is_external": true, + "cctx_gateway": 1 + }, + "Endpoint": "" + }, + "11155111": { + "Chain": { + "chain_id": 11155111, + "chain_name": 13, + "network_type": 1, + "vm": 1, + "is_external": true, + "cctx_gateway": 1 + }, + "Endpoint": "" + }, + "1337": { + "Chain": { + "chain_id": 1337, + "chain_name": 14, + "network_type": 2, + "vm": 1, + "is_external": true, + "cctx_gateway": 1 + }, + "Endpoint": "http://eth:8545" + }, + "5": { + "Chain": { + "chain_id": 5, + "chain_name": 6, + "network_type": 1, + "vm": 1, + "is_external": true, + "cctx_gateway": 1 + }, + "Endpoint": "" + }, + "56": { + "Chain": { + "chain_id": 56, + "chain_name": 5, + "network": 4, + "vm": 1, + "is_external": true, + "cctx_gateway": 1 + }, + "Endpoint": "" + }, + "80001": { + "Chain": { + "chain_id": 80001, + "chain_name": 7, + "network": 3, + "network_type": 1, + "vm": 1, + "is_external": true, + "cctx_gateway": 1 + }, + "Endpoint": "" + }, + "97": { + "Chain": { + "chain_id": 97, + "chain_name": 10, + "network": 4, + "network_type": 1, + "vm": 1, + "is_external": true, + "cctx_gateway": 1 + }, + "Endpoint": "" + } + }, + "BitcoinConfig": { + "RPCUsername": "smoketest", + "RPCPassword": "123", + "RPCHost": "bitcoin:18443", + "RPCParams": "regtest" + }, + "ComplianceConfig": { + "LogPath": "", + "RestrictedAddresses": [ + "0x8a81Ba8eCF2c418CAe624be726F505332DF119C6", + "bcrt1qzp4gt6fc7zkds09kfzaf9ln9c5rvrzxmy6qmpp" + ] + } +} diff --git a/zetaclient/testutils/mocks/chain_clients.go b/zetaclient/testutils/mocks/chain_clients.go index 44f1a9ea71..38690eda92 100644 --- a/zetaclient/testutils/mocks/chain_clients.go +++ b/zetaclient/testutils/mocks/chain_clients.go @@ -30,6 +30,18 @@ func (ob *EVMObserver) Start() { func (ob *EVMObserver) Stop() { } +func (ob *EVMObserver) OpenDB(_, _ string) error { + return nil +} + +func (ob *EVMObserver) LoadDB(_ string) error { + return nil +} + +func (ob *EVMObserver) SaveLastBlockScanned(_ uint64) error { + return nil +} + func (ob *EVMObserver) IsOutboundProcessed(_ *crosschaintypes.CrossChainTx, _ zerolog.Logger) (bool, bool, error) { return false, false, nil } @@ -71,6 +83,18 @@ func (ob *BTCObserver) Start() { func (ob *BTCObserver) Stop() { } +func (ob *BTCObserver) OpenDB(_, _ string) error { + return nil +} + +func (ob *BTCObserver) LoadDB(_ string) error { + return nil +} + +func (ob *BTCObserver) SaveLastBlockScanned(_ uint64) error { + return nil +} + func (ob *BTCObserver) IsOutboundProcessed(_ *crosschaintypes.CrossChainTx, _ zerolog.Logger) (bool, bool, error) { return false, false, nil } diff --git a/zetaclient/testutils/mocks/zetacore_client.go b/zetaclient/testutils/mocks/zetacore_client.go index 2f413a8764..bb4cae6b2d 100644 --- a/zetaclient/testutils/mocks/zetacore_client.go +++ b/zetaclient/testutils/mocks/zetacore_client.go @@ -5,6 +5,7 @@ import ( "math/big" "cosmossdk.io/math" + upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" "github.com/rs/zerolog" "github.com/zeta-chain/go-tss/blame" @@ -16,6 +17,7 @@ import ( 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" + clientcontext "github.com/zeta-chain/zetacore/zetaclient/context" keyinterfaces "github.com/zeta-chain/zetacore/zetaclient/keys/interfaces" "github.com/zeta-chain/zetacore/zetaclient/testutils" ) @@ -29,9 +31,15 @@ type MockZetacoreClient struct { paused bool zetaChain chains.Chain + // the mock block height + blockHeight int64 + // the mock observer keys keys keyinterfaces.ObserverKeys + // the mock upgrade plan + upgradePlan *upgradetypes.Plan + // the mock data for testing // pending cctxs pendingCctxs map[int64][]*crosschaintypes.CrossChainTx @@ -51,6 +59,48 @@ func NewMockZetacoreClient() *MockZetacoreClient { } } +func (m *MockZetacoreClient) UpdateAppContext(_ *clientcontext.AppContext, _ zerolog.Logger) error { + if m.paused { + return errors.New(ErrMsgPaused) + } + return nil +} + +func (m *MockZetacoreClient) GetUpgradePlan() (*upgradetypes.Plan, error) { + if m.paused { + return nil, errors.New(ErrMsgPaused) + } + return m.upgradePlan, nil +} + +func (m *MockZetacoreClient) GetChainParams() ([]*observerTypes.ChainParams, error) { + if m.paused { + return nil, errors.New(ErrMsgPaused) + } + return nil, nil +} + +func (m *MockZetacoreClient) GetSupportedChains() ([]chains.Chain, error) { + if m.paused { + return nil, errors.New(ErrMsgPaused) + } + return nil, nil +} + +func (m *MockZetacoreClient) GetCurrentTss() (observerTypes.TSS, error) { + if m.paused { + return observerTypes.TSS{}, errors.New(ErrMsgPaused) + } + return observerTypes.TSS{}, nil +} + +func (m *MockZetacoreClient) GetBlockHeaderEnabledChains() ([]lightclienttypes.HeaderSupportedChain, error) { + if m.paused { + return nil, errors.New(ErrMsgPaused) + } + return nil, nil +} + func (m *MockZetacoreClient) PostVoteInbound(_, _ uint64, _ *crosschaintypes.MsgVoteInbound) (string, string, error) { if m.paused { return "", "", errors.New(ErrMsgPaused) @@ -142,7 +192,7 @@ func (m *MockZetacoreClient) GetBlockHeight() (int64, error) { if m.paused { return 0, errors.New(ErrMsgPaused) } - return 0, nil + return m.blockHeight, nil } func (m *MockZetacoreClient) GetRateLimiterInput(_ int64) (crosschaintypes.QueryRateLimiterInputResponse, error) { @@ -262,6 +312,16 @@ func (m *MockZetacoreClient) WithKeys(keys keyinterfaces.ObserverKeys) *MockZeta return m } +func (m *MockZetacoreClient) WithBlockHeight(height int64) *MockZetacoreClient { + m.blockHeight = height + return m +} + +func (m *MockZetacoreClient) WithUpgradedPlan(plan *upgradetypes.Plan) *MockZetacoreClient { + m.upgradePlan = plan + return m +} + func (m *MockZetacoreClient) WithPendingCctx(chainID int64, cctxs []*crosschaintypes.CrossChainTx) *MockZetacoreClient { m.pendingCctxs[chainID] = cctxs return m diff --git a/zetaclient/testutils/testdata.go b/zetaclient/testutils/testdata.go index 620eb16b17..8ce792bbf1 100644 --- a/zetaclient/testutils/testdata.go +++ b/zetaclient/testutils/testdata.go @@ -22,6 +22,7 @@ const ( TestDataPathEVM = "testdata/evm" TestDataPathBTC = "testdata/btc" TestDataPathCctx = "testdata/cctx" + TestDataPathConfig = "testdata/config" RestrictedEVMAddressTest = "0x8a81Ba8eCF2c418CAe624be726F505332DF119C6" RestrictedBtcAddressTest = "bcrt1qzp4gt6fc7zkds09kfzaf9ln9c5rvrzxmy6qmpp" ) @@ -48,6 +49,13 @@ func LoadObjectFromJSONFile(t *testing.T, obj interface{}, filename string) { require.NoError(t, err) } +// LoadZetaclientConfig loads archived zetaclient config JSON file +func LoadZetaclientConfig(t *testing.T, dir string) config.Config { + config := config.Config{} + LoadObjectFromJSONFile(t, &config, path.Join(dir, TestDataPathConfig, ConfigFileName())) + return config +} + // ComplianceConfigTest returns a test compliance config // TODO(revamp): move to sample package func ComplianceConfigTest() config.ComplianceConfig { diff --git a/zetaclient/testutils/testdata_naming.go b/zetaclient/testutils/testdata_naming.go index f0345e347c..704c91935a 100644 --- a/zetaclient/testutils/testdata_naming.go +++ b/zetaclient/testutils/testdata_naming.go @@ -6,6 +6,11 @@ import ( "github.com/zeta-chain/zetacore/pkg/coin" ) +// ConfigFileName returns the archived zetaclient config file name +func ConfigFileName() string { + return "zetaclient_config.json" +} + // FileNameEVMBlock returns unified archive file name for block func FileNameEVMBlock(chainID int64, blockNumber uint64, trimmed bool) string { if !trimmed { diff --git a/zetaclient/tss/tss_signer.go b/zetaclient/tss/tss_signer.go index e00252db55..22885c66da 100644 --- a/zetaclient/tss/tss_signer.go +++ b/zetaclient/tss/tss_signer.go @@ -14,6 +14,7 @@ import ( "time" "github.com/binance-chain/tss-lib/ecdsa/keygen" + "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcutil" tmcrypto "github.com/cometbft/cometbft/crypto" @@ -27,7 +28,6 @@ import ( "github.com/zeta-chain/go-tss/p2p" "github.com/zeta-chain/go-tss/tss" - "github.com/zeta-chain/zetacore/pkg/chains" "github.com/zeta-chain/zetacore/pkg/cosmos" observertypes "github.com/zeta-chain/zetacore/x/observer/types" "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" @@ -83,9 +83,8 @@ type TSS struct { ZetacoreClient interfaces.ZetacoreClient KeysignsTracker *ConcurrentKeysignsTracker - // TODO: support multiple Bitcoin network, not just one network - // https://github.com/zeta-chain/node/issues/1397 - BitcoinChainID int64 + // BitcoinNetParams is the Bitcoin network parameters for the TSS to create BTC addresses + BitcoinNetParams *chaincfg.Params } // NewTSS creates a new TSS instance @@ -96,7 +95,6 @@ func NewTSS( preParams *keygen.LocalPreParams, client interfaces.ZetacoreClient, tssHistoricalList []observertypes.TSS, - bitcoinChainID int64, tssPassword string, hotkeyPassword string, ) (*TSS, error) { @@ -107,13 +105,13 @@ func NewTSS( } newTss := TSS{ - Server: server, - Keys: make(map[string]*Key), - CurrentPubkey: appContext.GetCurrentTssPubKey(), - logger: logger, - ZetacoreClient: client, - KeysignsTracker: NewKeysignsTracker(logger), - BitcoinChainID: bitcoinChainID, + Server: server, + Keys: make(map[string]*Key), + CurrentPubkey: appContext.GetCurrentTssPubkey(), + logger: logger, + ZetacoreClient: client, + KeysignsTracker: NewKeysignsTracker(logger), + BitcoinNetParams: appContext.GetBTCNetParams(), } err = newTss.LoadTssFilesFromDirectory(appContext.Config().TssPath) @@ -439,7 +437,7 @@ func (tss *TSS) EVMAddress() ethcommon.Address { // BTCAddress generates a bech32 p2wpkh address from pubkey func (tss *TSS) BTCAddress() string { - addr, err := GetTssAddrBTC(tss.CurrentPubkey, tss.BitcoinChainID) + addr, err := GetTssAddrBTC(tss.CurrentPubkey, tss.BitcoinNetParams) if err != nil { log.Error().Err(err).Msg("getKeyAddr error") return "" @@ -449,7 +447,7 @@ func (tss *TSS) BTCAddress() string { // BTCAddressWitnessPubkeyHash generates a bech32 p2wpkh address from pubkey func (tss *TSS) BTCAddressWitnessPubkeyHash() *btcutil.AddressWitnessPubKeyHash { - addrWPKH, err := getKeyAddrBTCWitnessPubkeyHash(tss.CurrentPubkey, tss.BitcoinChainID) + addrWPKH, err := getKeyAddrBTCWitnessPubkeyHash(tss.CurrentPubkey, tss.BitcoinNetParams) if err != nil { log.Error().Err(err).Msg("BTCAddressPubkeyHash error") return nil @@ -542,8 +540,8 @@ func (tss *TSS) LoadTssFilesFromDirectory(tssPath string) error { } // GetTssAddrBTC generates a bech32 p2wpkh address from pubkey -func GetTssAddrBTC(tssPubkey string, bitcoinChainID int64) (string, error) { - addrWPKH, err := getKeyAddrBTCWitnessPubkeyHash(tssPubkey, bitcoinChainID) +func GetTssAddrBTC(tssPubkey string, btcNetParams *chaincfg.Params) (string, error) { + addrWPKH, err := getKeyAddrBTCWitnessPubkeyHash(tssPubkey, btcNetParams) if err != nil { log.Fatal().Err(err) return "", err @@ -680,18 +678,16 @@ func wasNodePartOfTss(granteePubKey32 string, granteeList []string) bool { } // getKeyAddrBTCWitnessPubkeyHash generates a bech32 p2wpkh address from pubkey -func getKeyAddrBTCWitnessPubkeyHash(tssPubkey string, chainID int64) (*btcutil.AddressWitnessPubKeyHash, error) { +func getKeyAddrBTCWitnessPubkeyHash( + tssPubkey string, + btcNetParams *chaincfg.Params, +) (*btcutil.AddressWitnessPubKeyHash, error) { pubk, err := cosmos.GetPubKeyFromBech32(cosmos.Bech32PubKeyTypeAccPub, tssPubkey) if err != nil { return nil, err } - bitcoinNetParams, err := chains.BitcoinNetParamsFromChainID(chainID) - if err != nil { - return nil, err - } - - addr, err := btcutil.NewAddressWitnessPubKeyHash(btcutil.Hash160(pubk.Bytes()), bitcoinNetParams) + addr, err := btcutil.NewAddressWitnessPubKeyHash(btcutil.Hash160(pubk.Bytes()), btcNetParams) if err != nil { return nil, err } diff --git a/zetaclient/zetacore/client.go b/zetaclient/zetacore/client.go index 53d1d5958c..a8fffd9ea1 100644 --- a/zetaclient/zetacore/client.go +++ b/zetaclient/zetacore/client.go @@ -7,7 +7,9 @@ import ( "time" "cosmossdk.io/simapp/params" + "github.com/btcsuite/btcd/chaincfg" rpcclient "github.com/cometbft/cometbft/rpc/client" + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/pkg/errors" "github.com/rs/zerolog" "github.com/rs/zerolog/log" @@ -20,6 +22,7 @@ import ( "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" keyinterfaces "github.com/zeta-chain/zetacore/zetaclient/keys/interfaces" "github.com/zeta-chain/zetacore/zetaclient/metrics" ) @@ -39,8 +42,6 @@ type Client struct { 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 @@ -49,6 +50,39 @@ type Client struct { mockSDKClient rpcclient.Client } +// CreateClient is a helper function to create a new instance of Client +func CreateClient( + cfg config.Config, + telemetry *metrics.TelemetryServer, + hotkeyPassword string, +) (*Client, error) { + hotKey := cfg.AuthzHotkey + if cfg.HsmMode { + hotKey = cfg.HsmHotKey + } + + chainIP := cfg.ZetaCoreURL + + kb, _, err := keys.GetKeyringKeybase(cfg, hotkeyPassword) + if err != nil { + return nil, err + } + + granterAddreess, err := sdk.AccAddressFromBech32(cfg.AuthzGranter) + if err != nil { + return nil, err + } + + keys := keys.NewKeysWithKeybase(kb, granterAddreess, cfg.AuthzHotkey, hotkeyPassword) + + client, err := NewClient(keys, chainIP, hotKey, cfg.ChainID, cfg.HsmMode, telemetry) + if err != nil { + return nil, err + } + + return client, nil +} + // NewClient create a new instance of Client func NewClient( keys keyinterfaces.ObserverKeys, @@ -97,10 +131,8 @@ func NewClient( 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, @@ -134,11 +166,6 @@ func (c *Client) GetKeys() keyinterfaces.ObserverKeys { return c.keys } -func (c *Client) Stop() { - c.logger.Info().Msgf("zetacore client is stopping") - close(c.stop) // this notifies all configupdater to 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() @@ -191,110 +218,113 @@ func (c *Client) WaitForZetacoreToCreateBlocks() error { return nil } -// 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() - if err != nil { - return fmt.Errorf("failed to get zetablock height: %w", err) - } - plan, err := c.GetUpgradePlan() +// UpdateAppContext queries zetacore to update app context fields +func (c *Client) UpdateAppContext(appContext *context.AppContext, logger zerolog.Logger) error { + // get latest supported chains + supportedChains, err := c.GetSupportedChains() 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) + return errors.Wrap(err, "GetSupportedChains failed") } - 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 + supportedChainsMap := make(map[int64]chains.Chain) + for _, chain := range supportedChains { + supportedChainsMap[chain.ChainId] = chain } + // get additional chains that were dynamically to zetacore to support new chains additionalChains, err := c.GetAdditionalChains() if err != nil { return fmt.Errorf("failed to additional chains: %w", err) } + // get latest chain parameters chainParams, err := c.GetChainParams() if err != nil { - return fmt.Errorf("failed to get chain params: %w", err) + return errors.Wrap(err, "GetChainParams failed") } - newEVMParams := make(map[int64]*observertypes.ChainParams) - var newBTCParams *observertypes.ChainParams + var btcNetParams *chaincfg.Params + chainsEnabled := make([]chains.Chain, 0) + chainParamMap := make(map[int64]*observertypes.ChainParams) - // check and update chain params for each chain for _, chainParam := range chainParams { + // skip unsupported chain + if !chainParam.IsSupported { + continue + } + + // chain should exist in chain list + chain, found := supportedChainsMap[chainParam.ChainId] + if !found { + continue + } + + // skip ZetaChain + if !chain.IsExternalChain() { + continue + } + + // just in case (zetacore already validated) err := observertypes.ValidateChainParams(chainParam) if err != nil { - sampledLogger.Warn().Err(err).Msgf("Invalid chain params for chain %d", chainParam.ChainId) + logger.Error().Err(err).Msgf("Invalid chain params for chain %d", chainParam.ChainId) continue } + + // zetaclient detects Bitcoin network (regnet, testnet, mainnet) from chain params in zetacore + // The network params will be used by TSS to calculate the correct TSS address. if chains.IsBitcoinChain(chainParam.ChainId, additionalChains) { - newBTCParams = chainParam - } else if chains.IsEVMChain(chainParam.ChainId, additionalChains) { - newEVMParams[chainParam.ChainId] = chainParam + btcNetParams, err = chains.BitcoinNetParamsFromChainID(chainParam.ChainId) + if err != nil { + return errors.Wrapf(err, "BitcoinNetParamsFromChainID failed for chain %d", chainParam.ChainId) + } } - } - supportedChains, err := c.GetSupportedChains() - 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 + // zetaclient should observe this chain + chainsEnabled = append(chainsEnabled, chain) + chainParamMap[chainParam.ChainId] = chainParam } + + // get latest keygen keyGen, err := c.GetKeyGen() if err != nil { - c.logger.Info().Msg("Unable to fetch keygen from zetacore") - return fmt.Errorf("failed to get keygen: %w", err) + return errors.Wrap(err, "GetKeyGen failed") } + // get latest TSS public key tss, err := c.GetCurrentTss() 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) + return errors.Wrap(err, "GetCurrentTss failed") } - tssPubKey := tss.GetTssPubkey() + currentTssPubkey := tss.GetTssPubkey() + // get latest crosschain flags crosschainFlags, err := c.GetCrosschainFlags() 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) + return errors.Wrap(err, "GetCrosschainFlags failed") } + // get latest block header enabled chains blockHeaderEnabledChains, err := c.GetBlockHeaderEnabledChains() if err != nil { - c.logger.Info().Msg("Unable to fetch block header enabled chains from zetacore") - return err + return errors.Wrap(err, "GetBlockHeaderEnabledChains failed") } - coreContext.Update( - keyGen, - newChains, - newEVMParams, - newBTCParams, - tssPubKey, + // update app context fields + appContext.Update( + *keyGen, + currentTssPubkey, + chainsEnabled, + chainParamMap, + btcNetParams, crosschainFlags, additionalChains, blockHeaderEnabledChains, - init, + logger, ) return nil } -// Pause pauses the client -func (c *Client) Pause() { - <-c.pause -} - -// Unpause unpauses the client -func (c *Client) Unpause() { - c.pause <- struct{}{} -} - // EnableMockSDKClient enables the mock cosmos sdk client // TODO(revamp): move this to a test package func (c *Client) EnableMockSDKClient(client rpcclient.Client) { diff --git a/zetaclient/zetacore/tx.go b/zetaclient/zetacore/tx.go index 2ef142d03f..c320981249 100644 --- a/zetaclient/zetacore/tx.go +++ b/zetaclient/zetacore/tx.go @@ -10,7 +10,6 @@ import ( 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" @@ -20,7 +19,6 @@ import ( 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 @@ -174,28 +172,6 @@ func (c *Client) SetTSS(tssPubkey string, keyGenZetaHeight int64, status chains. 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) { diff --git a/zetaclient/zetacore/tx_test.go b/zetaclient/zetacore/tx_test.go index d53e93bda9..92672f442c 100644 --- a/zetaclient/zetacore/tx_test.go +++ b/zetaclient/zetacore/tx_test.go @@ -4,14 +4,16 @@ import ( "bytes" "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" + sdk "github.com/cosmos/cosmos-sdk/types" sdktypes "github.com/cosmos/cosmos-sdk/types" upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" ethtypes "github.com/ethereum/go-ethereum/core/types" @@ -203,7 +205,7 @@ func TestZetacore_SetTSS(t *testing.T) { }) } -func TestZetacore_UpdateZetacoreContext(t *testing.T) { +func TestZetacore_UpdateAppContext(t *testing.T) { //Setup server for multiple grpc calls listener, err := net.Listen("tcp", "127.0.0.1:9090") require.NoError(t, err) @@ -217,31 +219,39 @@ func TestZetacore_UpdateZetacoreContext(t *testing.T) { grpcmock.WithPlanner(planner.FirstMatch()), grpcmock.WithListener(listener), func(s *grpcmock.Server) { - method := "/zetachain.zetacore.crosschain.Query/LastZetaHeight" - s.ExpectUnary(method). - UnlimitedTimes(). - WithPayload(crosschaintypes.QueryLastZetaHeightRequest{}). - Return(crosschaintypes.QueryLastZetaHeightResponse{Height: 12345}) - - method = "/cosmos.upgrade.v1beta1.Query/CurrentPlan" - s.ExpectUnary(method). - UnlimitedTimes(). - WithPayload(upgradetypes.QueryCurrentPlanRequest{}). - Return(upgradetypes.QueryCurrentPlanResponse{ - Plan: &upgradetypes.Plan{ - Name: "big upgrade", - Height: 100, - }, - }) - - method = "/zetachain.zetacore.observer.Query/GetChainParams" + method := "/zetachain.zetacore.observer.Query/GetChainParams" s.ExpectUnary(method). UnlimitedTimes(). WithPayload(observertypes.QueryGetChainParamsRequest{}). Return(observertypes.QueryGetChainParamsResponse{ChainParams: &observertypes.ChainParamsList{ ChainParams: []*observertypes.ChainParams{ { - ChainId: 7000, + ChainId: chains.ZetaChainMainnet.ChainId, + IsSupported: true, + }, + { + ChainId: chains.Ethereum.ChainId, + ConfirmationCount: 32, + InboundTicker: 15, + OutboundTicker: 15, + GasPriceTicker: 300, + OutboundScheduleInterval: 64, + OutboundScheduleLookahead: 10, + BallotThreshold: sdk.MustNewDecFromStr("0.6"), + MinObserverDelegation: sdk.MustNewDecFromStr("1000000000000000000"), + IsSupported: true, + }, + { + ChainId: chains.BitcoinMainnet.ChainId, + ConfirmationCount: 32, + InboundTicker: 15, + OutboundTicker: 15, + GasPriceTicker: 300, + OutboundScheduleInterval: 64, + OutboundScheduleLookahead: 10, + BallotThreshold: sdk.MustNewDecFromStr("0.6"), + MinObserverDelegation: sdk.MustNewDecFromStr("1000000000000000000"), + IsSupported: true, }, }, }}) @@ -253,24 +263,18 @@ func TestZetacore_UpdateZetacoreContext(t *testing.T) { Return(observertypes.QuerySupportedChainsResponse{ Chains: []chains.Chain{ { - chains.BitcoinMainnet.ChainId, - chains.BitcoinMainnet.ChainName, - chains.BscMainnet.Network, - chains.BscMainnet.NetworkType, - chains.BscMainnet.Vm, - chains.BscMainnet.Consensus, - chains.BscMainnet.IsExternal, - chains.BscMainnet.CctxGateway, + ChainId: chains.ZetaChainMainnet.ChainId, + IsExternal: chains.ZetaChainMainnet.IsExternal, + }, + { + ChainId: chains.BitcoinMainnet.ChainId, + Consensus: chains.BitcoinMainnet.Consensus, + IsExternal: chains.BscMainnet.IsExternal, }, { - chains.Ethereum.ChainId, - chains.Ethereum.ChainName, - chains.Ethereum.Network, - chains.Ethereum.NetworkType, - chains.Ethereum.Vm, - chains.Ethereum.Consensus, - chains.Ethereum.IsExternal, - chains.Ethereum.CctxGateway, + ChainId: chains.Ethereum.ChainId, + Consensus: chains.Ethereum.Consensus, + IsExternal: chains.Ethereum.IsExternal, }, }, }) @@ -352,10 +356,20 @@ func TestZetacore_UpdateZetacoreContext(t *testing.T) { t.Run("zetacore update success", func(t *testing.T) { cfg := config.NewConfig() - appContext := context.New(cfg, zerolog.Nop()) + appCTX := context.New(cfg) zetacoreBroadcast = MockBroadcast - err := client.UpdateZetacoreContext(appContext, false, zerolog.Logger{}) + + // Update app context + err := client.UpdateAppContext(appCTX, zerolog.Logger{}) require.NoError(t, err) + + // Verify app context + require.Len(t, appCTX.GetEnabledExternalChainParams(), 2) + require.Len(t, appCTX.GetEnabledBTCChains(), 1) + chainParamMap := appCTX.GetEnabledExternalChainParams() + require.Len(t, chainParamMap, 2) + require.NotNil(t, chainParamMap[chains.Ethereum.ChainId]) + require.NotNil(t, chainParamMap[chains.BitcoinMainnet.ChainId]) }) }