From 3e8a83b563be857cc9a935cc794c6ae5e4529711 Mon Sep 17 00:00:00 2001 From: Likes To Eat Fish Date: Sat, 28 Sep 2024 16:56:05 +0700 Subject: [PATCH 01/37] add cmd multi-node --- ignite/cmd/testnet.go | 1 + ignite/cmd/testnet_multi_node.go | 145 +++++ ignite/config/chain/base/config.go | 20 + .../cmd/commands.go.plush | 2 + .../cmd/testnet_multi_node.go.plush | 505 ++++++++++++++++++ ignite/templates/app/files/config.yml.plush | 12 + 6 files changed, 685 insertions(+) create mode 100644 ignite/cmd/testnet_multi_node.go create mode 100644 ignite/templates/app/files/cmd/{{binaryNamePrefix}}d/cmd/testnet_multi_node.go.plush diff --git a/ignite/cmd/testnet.go b/ignite/cmd/testnet.go index 03e0068c06..6574cf8e48 100644 --- a/ignite/cmd/testnet.go +++ b/ignite/cmd/testnet.go @@ -18,6 +18,7 @@ func NewTestnet() *cobra.Command { c.AddCommand( NewTestnetInPlace(), + NewTestnetMultiNode(), ) return c diff --git a/ignite/cmd/testnet_multi_node.go b/ignite/cmd/testnet_multi_node.go new file mode 100644 index 0000000000..ea8d880111 --- /dev/null +++ b/ignite/cmd/testnet_multi_node.go @@ -0,0 +1,145 @@ +package ignitecmd + +import ( + "fmt" + + "github.com/spf13/cobra" + + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/ignite/cli/v29/ignite/pkg/cliui" + "github.com/ignite/cli/v29/ignite/pkg/cosmosaccount" + "github.com/ignite/cli/v29/ignite/pkg/errors" + "github.com/ignite/cli/v29/ignite/services/chain" +) + +func NewTestnetMultiNode() *cobra.Command { + c := &cobra.Command{ + Use: "multi-node", + Short: "Create a network test multi node", + Long: `Create a test network with the number of nodes from the config.yml file: + ... + multi-node: + validators: + - name: validator1 + stake: 100000000stake + - name: validator2 + stake: 200000000stake + - name: validator3 + stake: 200000000stake + - name: validator4 + stake: 200000000stake + + `, + Args: cobra.NoArgs, + RunE: testnetMultiNodeHandler, + } + flagSetPath(c) + flagSetClearCache(c) + c.Flags().AddFlagSet(flagSetHome()) + c.Flags().AddFlagSet(flagSetCheckDependencies()) + c.Flags().AddFlagSet(flagSetSkipProto()) + c.Flags().AddFlagSet(flagSetVerbose()) + + c.Flags().Bool(flagQuitOnFail, false, "quit program if the app fails to start") + return c +} + +func testnetMultiNodeHandler(cmd *cobra.Command, _ []string) error { + session := cliui.New( + cliui.WithVerbosity(getVerbosity(cmd)), + ) + defer session.End() + + // Otherwise run the serve command directly + return testnetInplace1(cmd, session) +} + +func testnetInplace1(cmd *cobra.Command, session *cliui.Session) error { + chainOption := []chain.Option{ + chain.WithOutputer(session), + chain.CollectEvents(session.EventBus()), + chain.CheckCosmosSDKVersion(), + } + + if flagGetCheckDependencies(cmd) { + chainOption = append(chainOption, chain.CheckDependencies()) + } + + // check if custom config is defined + config, _ := cmd.Flags().GetString(flagConfig) + if config != "" { + chainOption = append(chainOption, chain.ConfigFile(config)) + } + + c, err := chain.NewWithHomeFlags(cmd, chainOption...) + if err != nil { + return err + } + + cfg, err := c.Config() + if err != nil { + return err + } + home, err := c.Home() + if err != nil { + return err + } + keyringBackend, err := c.KeyringBackend() + if err != nil { + return err + } + ca, err := cosmosaccount.New( + cosmosaccount.WithKeyringBackend(cosmosaccount.KeyringBackend(keyringBackend)), + cosmosaccount.WithHome(home), + ) + if err != nil { + return err + } + + var ( + operatorAddress sdk.ValAddress + accounts string + accErr *cosmosaccount.AccountDoesNotExistError + ) + + fmt.Println(cfg.MultiNode.Validators) + + for _, acc := range cfg.Accounts { + sdkAcc, err := ca.GetByName(acc.Name) + if errors.As(err, &accErr) { + sdkAcc, _, err = ca.Create(acc.Name) + } + if err != nil { + return err + } + + sdkAddr, err := sdkAcc.Address(getAddressPrefix(cmd)) + if err != nil { + return err + } + if len(cfg.Validators) == 0 { + return errors.Errorf("no validators found for account %s", sdkAcc.Name) + } + if cfg.Validators[0].Name == acc.Name { + accAddr, err := sdk.AccAddressFromBech32(sdkAddr) + if err != nil { + return err + } + operatorAddress = sdk.ValAddress(accAddr) + } + accounts = accounts + "," + sdkAddr + } + + chainID, err := c.ID() + if err != nil { + return err + } + + args := chain.InPlaceArgs{ + NewChainID: chainID, + NewOperatorAddress: operatorAddress.String(), + AccountsToFund: accounts, + } + return c.TestnetInPlace(cmd.Context(), args) +} diff --git a/ignite/config/chain/base/config.go b/ignite/config/chain/base/config.go index a9e6340e63..df3ab5c004 100644 --- a/ignite/config/chain/base/config.go +++ b/ignite/config/chain/base/config.go @@ -141,6 +141,26 @@ type Config struct { Client Client `yaml:"client,omitempty" doc:"Configures client code generation."` Genesis xyaml.Map `yaml:"genesis,omitempty" doc:"Custom genesis block modifications. Follow the nesting of the genesis file here to access all the parameters."` Minimal bool `yaml:"minimal,omitempty" doc:"Indicates if the blockchain is minimal with the required Cosmos SDK modules."` + MultiNode MultiNode `yaml:"multi-node" doc:"Configuration for testnet multi node."` +} + +// Validator defines the configuration for a single validator. +type Validator struct { + Name string `yaml:"name" doc:"Name of the validator."` + Stake string `yaml:"stake" doc:"Amount of stake associated with the validator."` +} + +// RandomValidator defines the configuration for random validators. +type RandomValidator struct { + Count int `yaml:"count" doc:"Number of random validators to be generated."` + MinStake string `yaml:"min_stake" doc:"Minimum stake for each random validator."` + MaxStake string `yaml:"max_stake" doc:"Maximum stake for each random validator."` +} + +// MultiNode holds the configuration related to multiple validators and random validators. +type MultiNode struct { + Validators []Validator `yaml:"validators" doc:"List of manually configured validators."` + RandomValidators RandomValidator `yaml:"random_validators" doc:"Configuration for randomly generated validators."` } // GetVersion returns the config version. diff --git a/ignite/templates/app/files/cmd/{{binaryNamePrefix}}d/cmd/commands.go.plush b/ignite/templates/app/files/cmd/{{binaryNamePrefix}}d/cmd/commands.go.plush index 46df14a4b9..3f698c5ea0 100644 --- a/ignite/templates/app/files/cmd/{{binaryNamePrefix}}d/cmd/commands.go.plush +++ b/ignite/templates/app/files/cmd/{{binaryNamePrefix}}d/cmd/commands.go.plush @@ -20,6 +20,7 @@ import ( servertypes "github.com/cosmos/cosmos-sdk/server/types" "github.com/cosmos/cosmos-sdk/types/module" authcmd "github.com/cosmos/cosmos-sdk/x/auth/client/cli" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" "github.com/cosmos/cosmos-sdk/x/crisis" genutilcli "github.com/cosmos/cosmos-sdk/x/genutil/client/cli" "github.com/spf13/cobra" @@ -35,6 +36,7 @@ func initRootCmd( ) { rootCmd.AddCommand( genutilcli.InitCmd(basicManager, app.DefaultNodeHome), + NewTestnetMultiNodeCmd(basicManager, banktypes.GenesisBalancesIterator{}), NewInPlaceTestnetCmd(addModuleInitFlags), debug.Cmd(), confixcmd.ConfigCommand(), diff --git a/ignite/templates/app/files/cmd/{{binaryNamePrefix}}d/cmd/testnet_multi_node.go.plush b/ignite/templates/app/files/cmd/{{binaryNamePrefix}}d/cmd/testnet_multi_node.go.plush new file mode 100644 index 0000000000..7ec610e22f --- /dev/null +++ b/ignite/templates/app/files/cmd/{{binaryNamePrefix}}d/cmd/testnet_multi_node.go.plush @@ -0,0 +1,505 @@ +package cmd + +import ( + "bufio" + "encoding/json" + "fmt" + "io" + "strconv" + "strings" + + "os" + "path/filepath" + + cmtconfig "github.com/cometbft/cometbft/config" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + + "cosmossdk.io/math" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/client/tx" + "github.com/cosmos/cosmos-sdk/crypto/hd" + "github.com/cosmos/cosmos-sdk/crypto/keyring" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + "github.com/cosmos/cosmos-sdk/server" + srvconfig "github.com/cosmos/cosmos-sdk/server/config" + "github.com/cosmos/cosmos-sdk/testutil" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/cosmos/cosmos-sdk/x/genutil" + genutiltypes "github.com/cosmos/cosmos-sdk/x/genutil/types" + + tmrand "github.com/cometbft/cometbft/libs/rand" + types "github.com/cometbft/cometbft/types" + tmtime "github.com/cometbft/cometbft/types/time" + runtime "github.com/cosmos/cosmos-sdk/runtime" +) + +var ( + flagNodeDirPrefix = "node-dir-prefix" + flagNumValidators = "v" + flagOutputDir = "output-dir" + flagValidatorsStakeAmount = "validators-stake-amount" + flagStartingIPAddress = "starting-ip-address" +) + +const nodeDirPerm = 0o755 + +type initArgs struct { + algo string + chainID string + keyringBackend string + minGasPrices string + nodeDirPrefix string + numValidators int + outputDir string + startingIPAddress string + validatorsStakesAmount map[int]sdk.Coin +} + +// NewTestnetMultiNodeCmd returns a cmd to initialize all files for tendermint testnet and application +func NewTestnetMultiNodeCmd(mbm module.BasicManager, genBalIterator banktypes.GenesisBalancesIterator) *cobra.Command { + cmd := &cobra.Command{ + Use: "multi-node", + Short: "Initialize config directories & files for a multi-validator testnet running locally via separate processes (e.g. Docker Compose or similar)", + Long: `multi-node will setup "v" number of directories and populate each with +necessary files (private validator, genesis, config, etc.) for running "v" validator nodes. + +Booting up a network with these validator folders is intended to be used with Docker Compose, +or a similar setup where each node has a manually configurable IP address. + +Note, strict routability for addresses is turned off in the config file. + +Example: + <%= ModulePath %> multi-node --v 4 --output-dir ./.testnets --validators-stake-amount 1000000,200000,300000,400000 + `, + RunE: func(cmd *cobra.Command, _ []string) error { + clientCtx, err := client.GetClientQueryContext(cmd) + if err != nil { + return err + } + + serverCtx := server.GetServerContextFromCmd(cmd) + config := serverCtx.Config + + args := initArgs{} + args.outputDir, _ = cmd.Flags().GetString(flagOutputDir) + args.keyringBackend, _ = cmd.Flags().GetString(flags.FlagKeyringBackend) + args.chainID, _ = cmd.Flags().GetString(flags.FlagChainID) + args.minGasPrices, _ = cmd.Flags().GetString(server.FlagMinGasPrices) + args.nodeDirPrefix, _ = cmd.Flags().GetString(flagNodeDirPrefix) + args.startingIPAddress, _ = cmd.Flags().GetString(flagStartingIPAddress) + args.numValidators, _ = cmd.Flags().GetInt(flagNumValidators) + args.algo, _ = cmd.Flags().GetString(flags.FlagKeyType) + + args.validatorsStakesAmount = make(map[int]sdk.Coin) + top := 0 + // If the flag string is invalid, the amount will default to 100000000. + if s, err := cmd.Flags().GetString(flagValidatorsStakeAmount); err == nil { + for _, amount := range strings.Split(s, ",") { + a, ok := math.NewIntFromString(amount) + if !ok { + continue + } + args.validatorsStakesAmount[top] = sdk.NewCoin(sdk.DefaultBondDenom, a) + top += 1 + } + + } + + return initTestnetFiles(clientCtx, cmd, config, mbm, genBalIterator, args) + }, + } + + addTestnetFlagsToCmd(cmd) + cmd.Flags().String(flagNodeDirPrefix, "validator", "Prefix the directory name for each node with (node results in node0, node1, ...)") + cmd.Flags().String(flagValidatorsStakeAmount, "100000000,100000000,100000000,100000000", "Amount of stake for each validator") + cmd.Flags().String(flagStartingIPAddress, "localhost", "Starting IP address (192.168.0.1 results in persistent peers list ID0@192.168.0.1:46656, ID1@192.168.0.2:46656, ...)") + cmd.Flags().String(flags.FlagKeyringBackend, "test", "Select keyring's backend (os|file|test)") + + return cmd +} + +func addTestnetFlagsToCmd(cmd *cobra.Command) { + cmd.Flags().Int(flagNumValidators, 4, "Number of validators to initialize the testnet with") + cmd.Flags().StringP(flagOutputDir, "o", "./.testnets", "Directory to store initialization data for the testnet") + cmd.Flags().String(flags.FlagChainID, "", "genesis file chain-id, if left blank will be randomly created") + cmd.Flags().String(server.FlagMinGasPrices, fmt.Sprintf("0.0001%s", sdk.DefaultBondDenom), "Minimum gas prices to accept for transactions; All fees in a tx must meet this minimum (e.g. 0.01photino,0.001stake)") + cmd.Flags().String(flags.FlagKeyType, string(hd.Secp256k1Type), "Key signing algorithm to generate keys for") + + // support old flags name for backwards compatibility + cmd.Flags().SetNormalizeFunc(func(f *pflag.FlagSet, name string) pflag.NormalizedName { + if name == "algo" { + name = flags.FlagKeyType + } + + return pflag.NormalizedName(name) + }) +} + +// initTestnetFiles initializes testnet files for a testnet to be run in a separate process +func initTestnetFiles( + clientCtx client.Context, + cmd *cobra.Command, + nodeConfig *cmtconfig.Config, + mbm module.BasicManager, + genBalIterator banktypes.GenesisBalancesIterator, + args initArgs, +) error { + if args.chainID == "" { + args.chainID = "chain-" + tmrand.Str(6) + } + nodeIDs := make([]string, args.numValidators) + valPubKeys := make([]cryptotypes.PubKey, args.numValidators) + + simappConfig := srvconfig.DefaultConfig() + simappConfig.MinGasPrices = args.minGasPrices + simappConfig.API.Enable = false + simappConfig.BaseConfig.MinGasPrices = "0.0001" + sdk.DefaultBondDenom + simappConfig.Telemetry.EnableHostnameLabel = false + simappConfig.Telemetry.Enabled = false + simappConfig.Telemetry.PrometheusRetentionTime = 0 + + var ( + genAccounts []authtypes.GenesisAccount + genBalances []banktypes.Balance + genFiles []string + persistentPeers string + gentxsFiles []string + ) + + inBuf := bufio.NewReader(cmd.InOrStdin()) + // generate private keys, node IDs, and initial transactions + for i := 0; i < args.numValidators; i++ { + // validator1 + nodeDirName := fmt.Sprintf("%s%d", args.nodeDirPrefix, i) + // ../validator1 + nodeDir := filepath.Join(args.outputDir, nodeDirName) + // ../validator1/config/gentx/ + gentxsDir := filepath.Join(args.outputDir, nodeDirName, "config", "gentx") + + nodeConfig.SetRoot(nodeDir) + nodeConfig.Moniker = nodeDirName + nodeConfig.RPC.ListenAddress = "tcp://0.0.0.0:" + strconv.Itoa(26657-3*i) + + var err error + if err := os.MkdirAll(filepath.Join(nodeDir, "config"), nodeDirPerm); err != nil { + _ = os.RemoveAll(args.outputDir) + return err + } + + nodeIDs[i], valPubKeys[i], err = genutil.InitializeNodeValidatorFiles(nodeConfig) + if err != nil { + _ = os.RemoveAll(args.outputDir) + return err + } + + memo := fmt.Sprintf("%s@%s:"+strconv.Itoa(26656-3*i), nodeIDs[i], args.startingIPAddress) + + if persistentPeers == "" { + persistentPeers = memo + } else { + persistentPeers = persistentPeers + "," + memo + } + + genFiles = append(genFiles, nodeConfig.GenesisFile()) + + kb, err := keyring.New(sdk.KeyringServiceName(), args.keyringBackend, nodeDir, inBuf, clientCtx.Codec) + if err != nil { + return err + } + + keyringAlgos, _ := kb.SupportedAlgorithms() + algo, err := keyring.NewSigningAlgoFromString(args.algo, keyringAlgos) + if err != nil { + return err + } + + addr, secret, err := testutil.GenerateSaveCoinKey(kb, nodeDirName, "", true, algo) + if err != nil { + _ = os.RemoveAll(args.outputDir) + return err + } + + info := map[string]string{"secret": secret} + + cliPrint, err := json.Marshal(info) + if err != nil { + return err + } + + // save private key seed words + file := filepath.Join(nodeDir, fmt.Sprintf("%v.json", "key_seed")) + if err := writeFile(file, nodeDir, cliPrint); err != nil { + return err + } + + accTokens := sdk.TokensFromConsensusPower(1000, sdk.DefaultPowerReduction) + accStakingTokens := sdk.TokensFromConsensusPower(500, sdk.DefaultPowerReduction) + coins := sdk.Coins{ + sdk.NewCoin("testtoken", accTokens), + sdk.NewCoin(sdk.DefaultBondDenom, accStakingTokens), + } + + genBalances = append(genBalances, banktypes.Balance{Address: addr.String(), Coins: coins.Sort()}) + genAccounts = append(genAccounts, authtypes.NewBaseAccount(addr, nil, 0, 0)) + + var valTokens sdk.Coin + valTokens, ok := args.validatorsStakesAmount[i] + if !ok { + valTokens = sdk.NewCoin(sdk.DefaultBondDenom, sdk.TokensFromConsensusPower(100, sdk.DefaultPowerReduction)) + } + createValMsg, err := stakingtypes.NewMsgCreateValidator( + sdk.ValAddress(addr).String(), + valPubKeys[i], + valTokens, + stakingtypes.NewDescription(nodeDirName, "", "", "", ""), + stakingtypes.NewCommissionRates(math.LegacyOneDec(), math.LegacyOneDec(), math.LegacyOneDec()), + math.OneInt(), + ) + if err != nil { + return err + } + + txBuilder := clientCtx.TxConfig.NewTxBuilder() + if err := txBuilder.SetMsgs(createValMsg); err != nil { + return err + } + + txBuilder.SetMemo(memo) + + txFactory := tx.Factory{} + txFactory = txFactory. + WithChainID(args.chainID). + WithMemo(memo). + WithKeybase(kb). + WithTxConfig(clientCtx.TxConfig) + + if err := tx.Sign(cmd.Context(), txFactory, nodeDirName, txBuilder, true); err != nil { + return err + } + + txBz, err := clientCtx.TxConfig.TxJSONEncoder()(txBuilder.GetTx()) + if err != nil { + return err + } + file = filepath.Join(gentxsDir, fmt.Sprintf("%v.json", "gentx-"+nodeIDs[i])) + gentxsFiles = append(gentxsFiles, file) + if err := writeFile(file, gentxsDir, txBz); err != nil { + return err + } + + simappConfig.GRPC.Address = args.startingIPAddress + ":" + strconv.Itoa(9090-2*i) + simappConfig.API.Address = "tcp://localhost:" + strconv.Itoa(1317-i) + srvconfig.WriteConfigFile(filepath.Join(nodeDir, "config", "app.toml"), simappConfig) + } + + if err := initGenFiles(clientCtx, mbm, args.chainID, genAccounts, genBalances, genFiles, args.numValidators); err != nil { + return err + } + for i := 0; i < args.numValidators; i++ { + for _, file := range gentxsFiles { + nodeDirName := fmt.Sprintf("%s%d", args.nodeDirPrefix, i) + nodeDir := filepath.Join(args.outputDir, nodeDirName) + gentxsDir := filepath.Join(nodeDir, "config", "gentx") + + yes, err := isSubDir(file, gentxsDir) + if err != nil || yes { + continue + } + copyFile(file, gentxsDir) + } + } + err := collectGenFiles( + clientCtx, nodeConfig, args.chainID, nodeIDs, valPubKeys, args.numValidators, + args.outputDir, args.nodeDirPrefix, genBalIterator, + clientCtx.TxConfig.SigningContext().ValidatorAddressCodec(), + persistentPeers, + ) + if err != nil { + return err + } + + cmd.PrintErrf("Successfully initialized %d node directories\n", args.numValidators) + return nil +} + +func writeFile(file, dir string, contents []byte) error { + if err := os.MkdirAll(dir, 0o755); err != nil { + return fmt.Errorf("could not create directory %q: %w", dir, err) + } + + if err := os.WriteFile(file, contents, 0o644); err != nil { + return err + } + + return nil +} + +func initGenFiles( + clientCtx client.Context, mbm module.BasicManager, chainID string, + genAccounts []authtypes.GenesisAccount, genBalances []banktypes.Balance, + genFiles []string, numValidators int, +) error { + appGenState := mbm.DefaultGenesis(clientCtx.Codec) + + // set the accounts in the genesis state + var authGenState authtypes.GenesisState + clientCtx.Codec.MustUnmarshalJSON(appGenState[authtypes.ModuleName], &authGenState) + + accounts, err := authtypes.PackAccounts(genAccounts) + if err != nil { + return err + } + + authGenState.Accounts = accounts + appGenState[authtypes.ModuleName] = clientCtx.Codec.MustMarshalJSON(&authGenState) + + // set the balances in the genesis state + var bankGenState banktypes.GenesisState + clientCtx.Codec.MustUnmarshalJSON(appGenState[banktypes.ModuleName], &bankGenState) + + bankGenState.Balances = banktypes.SanitizeGenesisBalances(genBalances) + for _, bal := range bankGenState.Balances { + bankGenState.Supply = bankGenState.Supply.Add(bal.Coins...) + } + appGenState[banktypes.ModuleName] = clientCtx.Codec.MustMarshalJSON(&bankGenState) + + appGenStateJSON, err := json.MarshalIndent(appGenState, "", " ") + if err != nil { + return err + } + + genDoc := types.GenesisDoc{ + ChainID: chainID, + AppState: appGenStateJSON, + Validators: nil, + } + + // generate empty genesis files for each validator and save + for i := 0; i < numValidators; i++ { + if err := genDoc.SaveAs(genFiles[i]); err != nil { + return err + } + } + return nil +} + +func collectGenFiles( + clientCtx client.Context, nodeConfig *cmtconfig.Config, chainID string, + nodeIDs []string, valPubKeys []cryptotypes.PubKey, numValidators int, + outputDir, nodeDirPrefix string, genBalIterator banktypes.GenesisBalancesIterator, + valAddrCodec runtime.ValidatorAddressCodec, persistentPeers string, +) error { + var appState json.RawMessage + genTime := tmtime.Now() + + for i := 0; i < numValidators; i++ { + nodeDirName := fmt.Sprintf("%s%d", nodeDirPrefix, i) + nodeDir := filepath.Join(outputDir, nodeDirName) + gentxsDir := filepath.Join(nodeDir, "config", "gentx") + nodeConfig.Moniker = nodeDirName + + nodeConfig.SetRoot(nodeDir) + + nodeID, valPubKey := nodeIDs[i], valPubKeys[i] + initCfg := genutiltypes.NewInitConfig(chainID, gentxsDir, nodeID, valPubKey) + + appGenesis, err := genutiltypes.AppGenesisFromFile(nodeConfig.GenesisFile()) + if err != nil { + return err + } + + nodeAppState, err := genutil.GenAppStateFromConfig(clientCtx.Codec, clientCtx.TxConfig, nodeConfig, initCfg, appGenesis, genBalIterator, genutiltypes.DefaultMessageValidator, + valAddrCodec) + if err != nil { + return err + } + + nodeConfig.P2P.PersistentPeers = persistentPeers + nodeConfig.P2P.AllowDuplicateIP = true + nodeConfig.P2P.ListenAddress = "tcp://0.0.0.0:" + strconv.Itoa(26656-3*i) + nodeConfig.RPC.ListenAddress = "tcp://127.0.0.1:" + strconv.Itoa(26657-3*i) + nodeConfig.BaseConfig.ProxyApp = "tcp://127.0.0.1:" + strconv.Itoa(26658-3*i) + nodeConfig.Instrumentation.PrometheusListenAddr = ":" + strconv.Itoa(26660+i) + nodeConfig.Instrumentation.Prometheus = true + cmtconfig.WriteConfigFile(filepath.Join(nodeConfig.RootDir, "config", "config.toml"), nodeConfig) + if appState == nil { + // set the canonical application state (they should not differ) + appState = nodeAppState + } + + genFile := nodeConfig.GenesisFile() + + // overwrite each validator's genesis file to have a canonical genesis time + if err := genutil.ExportGenesisFileWithTime(genFile, chainID, nil, appState, genTime); err != nil { + return err + } + } + + return nil +} + +func copyFile(src, dstDir string) (int64, error) { + // Extract the file name from the source path + fileName := filepath.Base(src) + + // Create the full destination path (directory + file name) + dst := filepath.Join(dstDir, fileName) + + // Open the source file + sourceFile, err := os.Open(src) + if err != nil { + return 0, err + } + defer sourceFile.Close() + + // Create the destination file + destinationFile, err := os.Create(dst) + if err != nil { + return 0, err + } + defer destinationFile.Close() + + // Copy content from the source file to the destination file + bytesCopied, err := io.Copy(destinationFile, sourceFile) + if err != nil { + return 0, err + } + + // Ensure the content is written to the destination file + err = destinationFile.Sync() + if err != nil { + return 0, err + } + + return bytesCopied, nil +} + +// isSubDir checks if dstDir is a parent directory of src +func isSubDir(src, dstDir string) (bool, error) { + // Get the absolute path of src and dstDir + absSrc, err := filepath.Abs(src) + if err != nil { + return false, err + } + absDstDir, err := filepath.Abs(dstDir) + if err != nil { + return false, err + } + + // Check if absSrc is within absDstDir + relativePath, err := filepath.Rel(absDstDir, absSrc) + if err != nil { + return false, err + } + + // If the relative path doesn't go up the directory tree (doesn't contain ".."), it is inside dstDir + isInside := !strings.HasPrefix(relativePath, "..") && !filepath.IsAbs(relativePath) + return isInside, nil +} diff --git a/ignite/templates/app/files/config.yml.plush b/ignite/templates/app/files/config.yml.plush index 1bb23a6518..3586a554d4 100644 --- a/ignite/templates/app/files/config.yml.plush +++ b/ignite/templates/app/files/config.yml.plush @@ -20,3 +20,15 @@ faucet: validators: - name: alice bonded: 100000000stake +multi-node: + validators: + - name: validator1 + stake: 100000000stake + - name: validator2 + stake: 200000000stake + - name: validator3 + stake: 300000000stake + random_validators: + count: 3 + min_stake: 50000000stake + max_stake: 150000000stake \ No newline at end of file From 3e53b99cbb2fe96c8c6ee4e0cdd96eb2d1ffba04 Mon Sep 17 00:00:00 2001 From: Likes To Eat Fish Date: Sat, 28 Sep 2024 19:09:10 +0700 Subject: [PATCH 02/37] read config --- ignite/cmd/testnet_multi_node.go | 83 ++++++++----------- ignite/config/chain/base/config.go | 9 +- .../cmd/testnet_multi_node.go.plush | 2 +- ignite/templates/app/files/config.yml.plush | 3 +- 4 files changed, 43 insertions(+), 54 deletions(-) diff --git a/ignite/cmd/testnet_multi_node.go b/ignite/cmd/testnet_multi_node.go index ea8d880111..2be4e1cedc 100644 --- a/ignite/cmd/testnet_multi_node.go +++ b/ignite/cmd/testnet_multi_node.go @@ -2,14 +2,15 @@ package ignitecmd import ( "fmt" + "math/rand" + "cosmossdk.io/math" "github.com/spf13/cobra" sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ignite/cli/v29/ignite/config/chain/base" "github.com/ignite/cli/v29/ignite/pkg/cliui" - "github.com/ignite/cli/v29/ignite/pkg/cosmosaccount" - "github.com/ignite/cli/v29/ignite/pkg/errors" "github.com/ignite/cli/v29/ignite/services/chain" ) @@ -29,6 +30,13 @@ func NewTestnetMultiNode() *cobra.Command { stake: 200000000stake - name: validator4 stake: 200000000stake + or + .... + multi-node: + random_validators: + count: 4 + min_stake: 50000000stake + max_stake: 150000000stake `, Args: cobra.NoArgs, @@ -51,7 +59,6 @@ func testnetMultiNodeHandler(cmd *cobra.Command, _ []string) error { ) defer session.End() - // Otherwise run the serve command directly return testnetInplace1(cmd, session) } @@ -81,65 +88,45 @@ func testnetInplace1(cmd *cobra.Command, session *cliui.Session) error { if err != nil { return err } - home, err := c.Home() - if err != nil { - return err - } - keyringBackend, err := c.KeyringBackend() - if err != nil { - return err - } - ca, err := cosmosaccount.New( - cosmosaccount.WithKeyringBackend(cosmosaccount.KeyringBackend(keyringBackend)), - cosmosaccount.WithHome(home), - ) + + validatorDetails, err := getValidatorAmountStake(cfg.MultiNode) if err != nil { return err } + fmt.Println(validatorDetails) + fmt.Println(cfg.MultiNode.OutputDir) - var ( - operatorAddress sdk.ValAddress - accounts string - accErr *cosmosaccount.AccountDoesNotExistError - ) + return nil +} - fmt.Println(cfg.MultiNode.Validators) +func getValidatorAmountStake(cfg base.MultiNode) ([]math.Int, error) { + var amounts []math.Int - for _, acc := range cfg.Accounts { - sdkAcc, err := ca.GetByName(acc.Name) - if errors.As(err, &accErr) { - sdkAcc, _, err = ca.Create(acc.Name) - } + if len(cfg.Validators) == 0 { + numVal := cfg.RandomValidators.Count + minStake, err := sdk.ParseCoinNormalized(cfg.RandomValidators.MinStake) if err != nil { - return err + return amounts, err } - - sdkAddr, err := sdkAcc.Address(getAddressPrefix(cmd)) + maxStake, err := sdk.ParseCoinNormalized(cfg.RandomValidators.MaxStake) if err != nil { - return err + return amounts, err } - if len(cfg.Validators) == 0 { - return errors.Errorf("no validators found for account %s", sdkAcc.Name) + minS := minStake.Amount.Uint64() + maxS := maxStake.Amount.Uint64() + for i := 0; i < numVal; i++ { + stakeAmount := minS + rand.Uint64()%(maxS-minS+1) + amounts = append(amounts, math.NewIntFromUint64(stakeAmount)) } - if cfg.Validators[0].Name == acc.Name { - accAddr, err := sdk.AccAddressFromBech32(sdkAddr) + } else { + for _, v := range cfg.Validators { + stakeAmount, err := sdk.ParseCoinNormalized(v.Stake) if err != nil { - return err + return amounts, err } - operatorAddress = sdk.ValAddress(accAddr) + amounts = append(amounts, stakeAmount.Amount) } - accounts = accounts + "," + sdkAddr } - chainID, err := c.ID() - if err != nil { - return err - } - - args := chain.InPlaceArgs{ - NewChainID: chainID, - NewOperatorAddress: operatorAddress.String(), - AccountsToFund: accounts, - } - return c.TestnetInPlace(cmd.Context(), args) + return amounts, nil } diff --git a/ignite/config/chain/base/config.go b/ignite/config/chain/base/config.go index df3ab5c004..49f81a3244 100644 --- a/ignite/config/chain/base/config.go +++ b/ignite/config/chain/base/config.go @@ -145,13 +145,13 @@ type Config struct { } // Validator defines the configuration for a single validator. -type Validator struct { +type ValidatorDetails struct { Name string `yaml:"name" doc:"Name of the validator."` Stake string `yaml:"stake" doc:"Amount of stake associated with the validator."` } // RandomValidator defines the configuration for random validators. -type RandomValidator struct { +type RandomValidatorDetails struct { Count int `yaml:"count" doc:"Number of random validators to be generated."` MinStake string `yaml:"min_stake" doc:"Minimum stake for each random validator."` MaxStake string `yaml:"max_stake" doc:"Maximum stake for each random validator."` @@ -159,8 +159,9 @@ type RandomValidator struct { // MultiNode holds the configuration related to multiple validators and random validators. type MultiNode struct { - Validators []Validator `yaml:"validators" doc:"List of manually configured validators."` - RandomValidators RandomValidator `yaml:"random_validators" doc:"Configuration for randomly generated validators."` + Validators []ValidatorDetails `yaml:"validators" doc:"List of manually configured validators."` + RandomValidators RandomValidatorDetails `yaml:"random_validators" doc:"Configuration for randomly generated validators."` + OutputDir string `yaml:"output-dir" doc:"Directory to store initialization data for the testnet"` } // GetVersion returns the config version. diff --git a/ignite/templates/app/files/cmd/{{binaryNamePrefix}}d/cmd/testnet_multi_node.go.plush b/ignite/templates/app/files/cmd/{{binaryNamePrefix}}d/cmd/testnet_multi_node.go.plush index 7ec610e22f..e1868ef090 100644 --- a/ignite/templates/app/files/cmd/{{binaryNamePrefix}}d/cmd/testnet_multi_node.go.plush +++ b/ignite/templates/app/files/cmd/{{binaryNamePrefix}}d/cmd/testnet_multi_node.go.plush @@ -76,7 +76,7 @@ or a similar setup where each node has a manually configurable IP address. Note, strict routability for addresses is turned off in the config file. Example: - <%= ModulePath %> multi-node --v 4 --output-dir ./.testnets --validators-stake-amount 1000000,200000,300000,400000 + <%= AppName %>d multi-node --v 4 --output-dir ./.testnets --validators-stake-amount 1000000,200000,300000,400000 `, RunE: func(cmd *cobra.Command, _ []string) error { clientCtx, err := client.GetClientQueryContext(cmd) diff --git a/ignite/templates/app/files/config.yml.plush b/ignite/templates/app/files/config.yml.plush index 3586a554d4..8fc7629d9d 100644 --- a/ignite/templates/app/files/config.yml.plush +++ b/ignite/templates/app/files/config.yml.plush @@ -31,4 +31,5 @@ multi-node: random_validators: count: 3 min_stake: 50000000stake - max_stake: 150000000stake \ No newline at end of file + max_stake: 150000000stake + output-dir: $HOME/.<%= AppName %>d \ No newline at end of file From dfb928ea765dabc2577281a4fa2e100c40d617b8 Mon Sep 17 00:00:00 2001 From: Likes To Eat Fish Date: Sat, 28 Sep 2024 19:14:32 +0700 Subject: [PATCH 03/37] minor --- ignite/templates/app/files/config.yml.plush | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ignite/templates/app/files/config.yml.plush b/ignite/templates/app/files/config.yml.plush index 8fc7629d9d..be416f403d 100644 --- a/ignite/templates/app/files/config.yml.plush +++ b/ignite/templates/app/files/config.yml.plush @@ -32,4 +32,5 @@ multi-node: count: 3 min_stake: 50000000stake max_stake: 150000000stake - output-dir: $HOME/.<%= AppName %>d \ No newline at end of file + output-dir: $HOME/.<%= AppName %>d + chain-id: <%= AppName %>-test-1 \ No newline at end of file From df91385f0669a64c55a7f232727b5c8e4361c6c7 Mon Sep 17 00:00:00 2001 From: Likes To Eat Fish Date: Sat, 28 Sep 2024 22:37:23 +0700 Subject: [PATCH 04/37] init node --- ignite/cmd/testnet_multi_node.go | 48 +++++++++++------ ignite/config/chain/base/config.go | 1 + ignite/pkg/chaincmd/chaincmd.go | 4 ++ ignite/pkg/chaincmd/in-place-testnet.go | 54 +++++++++++++++++++ ignite/pkg/chaincmd/runner/chain.go | 12 +++++ ignite/services/chain/runtime.go | 10 ++++ ignite/services/chain/testnet.go | 29 ++++++++++ .../cmd/testnet_multi_node.go.plush | 1 + ignite/templates/app/files/config.yml.plush | 2 +- 9 files changed, 145 insertions(+), 16 deletions(-) diff --git a/ignite/cmd/testnet_multi_node.go b/ignite/cmd/testnet_multi_node.go index 2be4e1cedc..f4f382aa29 100644 --- a/ignite/cmd/testnet_multi_node.go +++ b/ignite/cmd/testnet_multi_node.go @@ -1,8 +1,8 @@ package ignitecmd import ( - "fmt" "math/rand" + "strconv" "cosmossdk.io/math" "github.com/spf13/cobra" @@ -59,10 +59,10 @@ func testnetMultiNodeHandler(cmd *cobra.Command, _ []string) error { ) defer session.End() - return testnetInplace1(cmd, session) + return testnetMultiNode(cmd, session) } -func testnetInplace1(cmd *cobra.Command, session *cliui.Session) error { +func testnetMultiNode(cmd *cobra.Command, session *cliui.Session) error { chainOption := []chain.Option{ chain.WithOutputer(session), chain.CollectEvents(session.EventBus()), @@ -89,44 +89,62 @@ func testnetInplace1(cmd *cobra.Command, session *cliui.Session) error { return err } - validatorDetails, err := getValidatorAmountStake(cfg.MultiNode) + numVal, amountDetails, err := getValidatorAmountStake(cfg.MultiNode) if err != nil { return err } - fmt.Println(validatorDetails) - fmt.Println(cfg.MultiNode.OutputDir) + args := chain.MultiNodeArgs{ + ChainID: cfg.MultiNode.ChainID, + ValidatorsStakeAmount: amountDetails, + OutputDir: cfg.MultiNode.OutputDir, + NumValidator: strconv.Itoa(numVal), + } - return nil + return c.TestnetMultiNode(cmd.Context(), args) } -func getValidatorAmountStake(cfg base.MultiNode) ([]math.Int, error) { - var amounts []math.Int +// getValidatorAmountStake returns the number of validators and the amountStakes arg from config.MultiNode +func getValidatorAmountStake(cfg base.MultiNode) (int, string, error) { + var amounts string + count := 0 if len(cfg.Validators) == 0 { numVal := cfg.RandomValidators.Count minStake, err := sdk.ParseCoinNormalized(cfg.RandomValidators.MinStake) if err != nil { - return amounts, err + return count, amounts, err } maxStake, err := sdk.ParseCoinNormalized(cfg.RandomValidators.MaxStake) if err != nil { - return amounts, err + return count, amounts, err } minS := minStake.Amount.Uint64() maxS := maxStake.Amount.Uint64() for i := 0; i < numVal; i++ { stakeAmount := minS + rand.Uint64()%(maxS-minS+1) - amounts = append(amounts, math.NewIntFromUint64(stakeAmount)) + if amounts == "" { + amounts = math.NewIntFromUint64(stakeAmount).String() + count += 1 + } else { + amounts = amounts + "," + math.NewIntFromUint64(stakeAmount).String() + count += 1 + } } } else { for _, v := range cfg.Validators { stakeAmount, err := sdk.ParseCoinNormalized(v.Stake) if err != nil { - return amounts, err + return count, amounts, err + } + if amounts == "" { + amounts = stakeAmount.Amount.String() + count += 1 + } else { + amounts = amounts + "," + stakeAmount.Amount.String() + count += 1 } - amounts = append(amounts, stakeAmount.Amount) } } - return amounts, nil + return count, amounts, nil } diff --git a/ignite/config/chain/base/config.go b/ignite/config/chain/base/config.go index 49f81a3244..935e3b2674 100644 --- a/ignite/config/chain/base/config.go +++ b/ignite/config/chain/base/config.go @@ -162,6 +162,7 @@ type MultiNode struct { Validators []ValidatorDetails `yaml:"validators" doc:"List of manually configured validators."` RandomValidators RandomValidatorDetails `yaml:"random_validators" doc:"Configuration for randomly generated validators."` OutputDir string `yaml:"output-dir" doc:"Directory to store initialization data for the testnet"` + ChainID string `yaml:"chain-id" doc:"Directory to store initialization data for the testnet"` } // GetVersion returns the config version. diff --git a/ignite/pkg/chaincmd/chaincmd.go b/ignite/pkg/chaincmd/chaincmd.go index c45baf53fc..e7c9f78ab5 100644 --- a/ignite/pkg/chaincmd/chaincmd.go +++ b/ignite/pkg/chaincmd/chaincmd.go @@ -28,6 +28,7 @@ const ( commandExport = "export" commandTendermint = "tendermint" commandTestnetInPlace = "in-place-testnet" + commandTestnetMultiNode = "multi-node" optionHome = "--home" optionNode = "--node" @@ -59,6 +60,9 @@ const ( optionValidatorPrivateKey = "--validator-privkey" optionAccountToFund = "--accounts-to-fund" optionSkipConfirmation = "--skip-confirmation" + optionAmountStakes = "--validators-stake-amount" + optionOutPutDir = "--output-dir" + optionNumValidator = "--v" constTendermint = "tendermint" constJSON = "json" diff --git a/ignite/pkg/chaincmd/in-place-testnet.go b/ignite/pkg/chaincmd/in-place-testnet.go index 84a3b38465..19e150ce59 100644 --- a/ignite/pkg/chaincmd/in-place-testnet.go +++ b/ignite/pkg/chaincmd/in-place-testnet.go @@ -1,6 +1,8 @@ package chaincmd import ( + "fmt" + "github.com/ignite/cli/v29/ignite/pkg/cmdrunner/step" ) @@ -45,3 +47,55 @@ func (c ChainCmd) TestnetInPlaceCommand(newChainID, newOperatorAddress string, o return c.daemonCommand(command) } + +type MultiNodeOption func([]string) []string + +func MultiNodeWithChainID(ChainId string) MultiNodeOption { + return func(s []string) []string { + if len(ChainId) > 0 { + return append(s, optionChainID, ChainId) + } + return s + } +} + +func MultiNodeWithDirOutput(dirOutput string) MultiNodeOption { + return func(s []string) []string { + if len(dirOutput) > 0 { + return append(s, optionOutPutDir, dirOutput) + } + return s + } +} + +func MultiNodeWithNumValidator(numVal string) MultiNodeOption { + return func(s []string) []string { + if len(numVal) > 0 { + return append(s, optionNumValidator, numVal) + } + return s + } +} +func MultiNodeWithValidatorsStakeAmount(satkeAmounts string) MultiNodeOption { + return func(s []string) []string { + if len(satkeAmounts) > 0 { + return append(s, optionAmountStakes, satkeAmounts) + } + return s + } +} + +// TestnetMultiNodeCommand return command to start testnet multinode. +func (c ChainCmd) TestnetMultiNodeCommand(options ...MultiNodeOption) step.Option { + command := []string{ + commandTestnetMultiNode, + } + + // Apply the options provided by the user + for _, apply := range options { + command = apply(command) + } + fmt.Println(command) + + return c.daemonCommand(command) +} diff --git a/ignite/pkg/chaincmd/runner/chain.go b/ignite/pkg/chaincmd/runner/chain.go index 0b133a47d2..ea39c315ac 100644 --- a/ignite/pkg/chaincmd/runner/chain.go +++ b/ignite/pkg/chaincmd/runner/chain.go @@ -57,6 +57,18 @@ func (r Runner) InPlace(ctx context.Context, newChainID, newOperatorAddress stri ) } +func (r Runner) MultiNode(ctx context.Context, options ...chaincmd.MultiNodeOption) error { + runOptions := runOptions{ + stdout: os.Stdout, + stderr: os.Stderr, + } + return r.run( + ctx, + runOptions, + r.chainCmd.TestnetMultiNodeCommand(options...), + ) +} + // Gentx generates a genesis tx carrying a self delegation. func (r Runner) Gentx( ctx context.Context, diff --git a/ignite/services/chain/runtime.go b/ignite/services/chain/runtime.go index a0eb21e1df..b558eb2d7b 100644 --- a/ignite/services/chain/runtime.go +++ b/ignite/services/chain/runtime.go @@ -48,6 +48,16 @@ func (c Chain) InPlace(ctx context.Context, runner chaincmdrunner.Runner, args I return err } +func (c Chain) MultiNode(ctx context.Context, runner chaincmdrunner.Runner, args MultiNodeArgs) error { + err := runner.MultiNode(ctx, + chaincmd.MultiNodeWithChainID(args.ChainID), + chaincmd.MultiNodeWithDirOutput(args.OutputDir), + chaincmd.MultiNodeWithNumValidator(args.NumValidator), + chaincmd.MultiNodeWithValidatorsStakeAmount(args.ValidatorsStakeAmount), + ) + return err +} + // Start wraps the "appd start" command to begin running a chain from the daemon. func (c Chain) Start(ctx context.Context, runner chaincmdrunner.Runner, cfg *chainconfig.Config) error { validator, err := chainconfig.FirstValidator(cfg) diff --git a/ignite/services/chain/testnet.go b/ignite/services/chain/testnet.go index 5a3b85d77b..e60e94aa73 100644 --- a/ignite/services/chain/testnet.go +++ b/ignite/services/chain/testnet.go @@ -35,3 +35,32 @@ func (c Chain) TestnetInPlace(ctx context.Context, args InPlaceArgs) error { } return nil } + +type MultiNodeArgs struct { + ChainID string + OutputDir string + NumValidator string + ValidatorsStakeAmount string +} + +func (c Chain) TestnetMultiNode(ctx context.Context, args MultiNodeArgs) error { + commands, err := c.Commands(ctx) + if err != nil { + return err + } + + // make sure that config.yml exists + if c.options.ConfigFile != "" { + if _, err := os.Stat(c.options.ConfigFile); err != nil { + return err + } + } else if _, err := chainconfig.LocateDefault(c.app.Path); err != nil { + return err + } + + err = c.MultiNode(ctx, commands, args) + if err != nil { + return err + } + return nil +} diff --git a/ignite/templates/app/files/cmd/{{binaryNamePrefix}}d/cmd/testnet_multi_node.go.plush b/ignite/templates/app/files/cmd/{{binaryNamePrefix}}d/cmd/testnet_multi_node.go.plush index e1868ef090..978d873a41 100644 --- a/ignite/templates/app/files/cmd/{{binaryNamePrefix}}d/cmd/testnet_multi_node.go.plush +++ b/ignite/templates/app/files/cmd/{{binaryNamePrefix}}d/cmd/testnet_multi_node.go.plush @@ -302,6 +302,7 @@ func initTestnetFiles( if err := initGenFiles(clientCtx, mbm, args.chainID, genAccounts, genBalances, genFiles, args.numValidators); err != nil { return err } + // copy gentx file for i := 0; i < args.numValidators; i++ { for _, file := range gentxsFiles { nodeDirName := fmt.Sprintf("%s%d", args.nodeDirPrefix, i) diff --git a/ignite/templates/app/files/config.yml.plush b/ignite/templates/app/files/config.yml.plush index be416f403d..f2b28c4904 100644 --- a/ignite/templates/app/files/config.yml.plush +++ b/ignite/templates/app/files/config.yml.plush @@ -32,5 +32,5 @@ multi-node: count: 3 min_stake: 50000000stake max_stake: 150000000stake - output-dir: $HOME/.<%= AppName %>d + output-dir: ./.<%= AppName %>-testnet/ chain-id: <%= AppName %>-test-1 \ No newline at end of file From 48d336d41fe11314d6067dfa6573fc8c607899da Mon Sep 17 00:00:00 2001 From: Likes To Eat Fish Date: Sun, 29 Sep 2024 23:13:10 +0700 Subject: [PATCH 05/37] done --- ignite/cmd/model/testnet_multi_node.go | 161 ++++++++++++++++++++++++ ignite/cmd/testnet_multi_node.go | 18 ++- ignite/pkg/chaincmd/chaincmd.go | 6 + ignite/pkg/chaincmd/in-place-testnet.go | 23 ++++ ignite/pkg/chaincmd/runner/chain.go | 13 ++ ignite/services/chain/runtime.go | 56 +++++++++ ignite/services/chain/testnet.go | 23 ++++ 7 files changed, 299 insertions(+), 1 deletion(-) create mode 100644 ignite/cmd/model/testnet_multi_node.go diff --git a/ignite/cmd/model/testnet_multi_node.go b/ignite/cmd/model/testnet_multi_node.go new file mode 100644 index 0000000000..d488e81885 --- /dev/null +++ b/ignite/cmd/model/testnet_multi_node.go @@ -0,0 +1,161 @@ +package cmdmodel + +import ( + "context" + "fmt" + "os/exec" + "path/filepath" + "strconv" + "syscall" + + tea "github.com/charmbracelet/bubbletea" + "github.com/ignite/cli/v29/ignite/services/chain" +) + +type NodeStatus int + +const ( + Stopped NodeStatus = iota + Running +) + +type Model struct { + appd string + args chain.MultiNodeArgs + ctx context.Context + + nodeStatuses []NodeStatus + pids []int // Store the PIDs of the running processes + numNodes int // Number of nodes +} + +type ToggleNodeMsg struct { + nodeIdx int +} + +type UpdateStatusMsg struct { + nodeIdx int + status NodeStatus +} + +// Initialize the model +func NewModel(chainname string, ctx context.Context, args chain.MultiNodeArgs) Model { + numNodes, err := strconv.Atoi(args.NumValidator) + if err != nil { + panic(err) + } + return Model{ + appd: chainname + "d", + args: args, + ctx: ctx, + nodeStatuses: make([]NodeStatus, numNodes), // initial states of nodes + pids: make([]int, numNodes), + numNodes: numNodes, + } +} + +// Implement the Update function +func (m Model) Init() tea.Cmd { + return nil +} + +// ToggleNode toggles the state of a node +func ToggleNode(nodeIdx int) tea.Cmd { + return func() tea.Msg { + return ToggleNodeMsg{nodeIdx: nodeIdx} + } +} + +// Run or stop the node based on its status +func RunNode(nodeIdx int, start bool, pid *int, args chain.MultiNodeArgs, appd string) tea.Cmd { + return func() tea.Msg { + if start { + nodeHome := filepath.Join(args.OutputDir, args.NodeDirPrefix+strconv.Itoa(nodeIdx)) + // Create the command to run in background as a daemon + cmd := exec.Command(appd, "start", "--home", nodeHome) + + // Start the process as a daemon + cmd.SysProcAttr = &syscall.SysProcAttr{ + Setpgid: true, // Ensure it runs in a new process group + } + + err := cmd.Start() // Start the node in the background + if err != nil { + fmt.Printf("Failed to start node %d: %v\n", nodeIdx+1, err) + return UpdateStatusMsg{nodeIdx: nodeIdx, status: Stopped} + } + + *pid = cmd.Process.Pid // Store the PID + go cmd.Wait() // Let the process run asynchronously + return UpdateStatusMsg{nodeIdx: nodeIdx, status: Running} + } else { + // Use kill to stop the node process by PID + if *pid != 0 { + err := syscall.Kill(-*pid, syscall.SIGTERM) // Stop the daemon process + if err != nil { + fmt.Printf("Failed to stop node %d: %v\n", nodeIdx+1, err) + } else { + *pid = 0 // Reset PID after stopping + } + } + return UpdateStatusMsg{nodeIdx: nodeIdx, status: Stopped} + } + } +} + +// Stop all nodes +func (m *Model) StopAllNodes() { + for i := 0; i < m.numNodes; i++ { + if m.nodeStatuses[i] == Running { + RunNode(i, false, &m.pids[i], m.args, m.appd)() // Stop node + } + } +} + +// Update handles messages and updates the model +func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + case "q": + m.StopAllNodes() // Stop all nodes before quitting + return m, tea.Quit + default: + // Check for numbers from 1 to numNodes + for i := 0; i < m.numNodes; i++ { + if msg.String() == fmt.Sprintf("%d", i+1) { + return m, ToggleNode(i) + } + } + } + + case ToggleNodeMsg: + if m.nodeStatuses[msg.nodeIdx] == Running { + return m, RunNode(msg.nodeIdx, false, &m.pids[msg.nodeIdx], m.args, m.appd) // Stop node + } + return m, RunNode(msg.nodeIdx, true, &m.pids[msg.nodeIdx], m.args, m.appd) // Start node + + case UpdateStatusMsg: + m.nodeStatuses[msg.nodeIdx] = msg.status + return m, nil + } + + return m, nil +} + +// View renders the interface +func (m Model) View() string { + statusText := func(status NodeStatus) string { + if status == Running { + return "[Running]" + } + return "[Stopped]" + } + + output := "Node Control:\n" + for i := 0; i < m.numNodes; i++ { + output += fmt.Sprintf("%d. Node %d %s\n", i+1, i+1, statusText(m.nodeStatuses[i])) + } + output += "Press q to quit.\n" + return output +} diff --git a/ignite/cmd/testnet_multi_node.go b/ignite/cmd/testnet_multi_node.go index f4f382aa29..30c897bb5f 100644 --- a/ignite/cmd/testnet_multi_node.go +++ b/ignite/cmd/testnet_multi_node.go @@ -1,14 +1,18 @@ package ignitecmd import ( + // "fmt" "math/rand" "strconv" + "time" "cosmossdk.io/math" "github.com/spf13/cobra" sdk "github.com/cosmos/cosmos-sdk/types" + tea "github.com/charmbracelet/bubbletea" + cmdmodel "github.com/ignite/cli/v29/ignite/cmd/model" "github.com/ignite/cli/v29/ignite/config/chain/base" "github.com/ignite/cli/v29/ignite/pkg/cliui" "github.com/ignite/cli/v29/ignite/services/chain" @@ -98,9 +102,21 @@ func testnetMultiNode(cmd *cobra.Command, session *cliui.Session) error { ValidatorsStakeAmount: amountDetails, OutputDir: cfg.MultiNode.OutputDir, NumValidator: strconv.Itoa(numVal), + NodeDirPrefix: "validator", //node } - return c.TestnetMultiNode(cmd.Context(), args) + //initialized 3 node directories + err = c.TestnetMultiNode(cmd.Context(), args) + if err != nil { + return err + } + + time.Sleep(7 * time.Second) + + // c.TestnetStartMultiNode(cmd.Context(), args) + m := cmdmodel.NewModel(c.Name(), cmd.Context(), args) + _, err = tea.NewProgram(m).Run() + return err } // getValidatorAmountStake returns the number of validators and the amountStakes arg from config.MultiNode diff --git a/ignite/pkg/chaincmd/chaincmd.go b/ignite/pkg/chaincmd/chaincmd.go index e7c9f78ab5..057f4445d0 100644 --- a/ignite/pkg/chaincmd/chaincmd.go +++ b/ignite/pkg/chaincmd/chaincmd.go @@ -181,6 +181,7 @@ func (c ChainCmd) StartCommand(options ...string) step.Option { command := append([]string{ commandStart, }, options...) + fmt.Println(command) return c.daemonCommand(command) } @@ -643,6 +644,11 @@ func (c ChainCmd) daemonCommand(command []string) step.Option { return step.Exec(c.appCmd, c.attachHome(command)...) } +// daemonCommand returns the daemon command from the given command (including the home flag). +func (c ChainCmd) daemonCommandIncludedHomeFlag(command []string) step.Option { + return step.Exec(c.appCmd, command...) +} + // cliCommand returns the cli command from the provided command. func (c ChainCmd) cliCommand(command []string) step.Option { return step.Exec(c.appCmd, c.attachHome(command)...) diff --git a/ignite/pkg/chaincmd/in-place-testnet.go b/ignite/pkg/chaincmd/in-place-testnet.go index 19e150ce59..f4ecf360bb 100644 --- a/ignite/pkg/chaincmd/in-place-testnet.go +++ b/ignite/pkg/chaincmd/in-place-testnet.go @@ -85,6 +85,15 @@ func MultiNodeWithValidatorsStakeAmount(satkeAmounts string) MultiNodeOption { } } +func MultiNodeWithHome(home string) MultiNodeOption { + return func(s []string) []string { + if len(home) > 0 { + return append(s, optionHome, home) + } + return s + } +} + // TestnetMultiNodeCommand return command to start testnet multinode. func (c ChainCmd) TestnetMultiNodeCommand(options ...MultiNodeOption) step.Option { command := []string{ @@ -99,3 +108,17 @@ func (c ChainCmd) TestnetMultiNodeCommand(options ...MultiNodeOption) step.Optio return c.daemonCommand(command) } + +// TestnetMultiNodeCommand return command to start testnet multinode. +func (c ChainCmd) TestnetStartMultiNodeCommand(options ...MultiNodeOption) step.Option { + command := []string{ + commandStart, + } + + // Apply the options provided by the user + for _, apply := range options { + command = apply(command) + } + + return c.daemonCommandIncludedHomeFlag(command) +} diff --git a/ignite/pkg/chaincmd/runner/chain.go b/ignite/pkg/chaincmd/runner/chain.go index ea39c315ac..ad7e099419 100644 --- a/ignite/pkg/chaincmd/runner/chain.go +++ b/ignite/pkg/chaincmd/runner/chain.go @@ -69,6 +69,19 @@ func (r Runner) MultiNode(ctx context.Context, options ...chaincmd.MultiNodeOpti ) } +func (r Runner) StartMultiNode(ctx context.Context, options ...chaincmd.MultiNodeOption) error { + runOptions := runOptions{ + wrappedStdErrMaxLen: 50000, + stdout: os.Stdout, + stderr: os.Stderr, + } + return r.run( + ctx, + runOptions, + r.chainCmd.TestnetStartMultiNodeCommand(options...), + ) +} + // Gentx generates a genesis tx carrying a self delegation. func (r Runner) Gentx( ctx context.Context, diff --git a/ignite/services/chain/runtime.go b/ignite/services/chain/runtime.go index b558eb2d7b..739347df7c 100644 --- a/ignite/services/chain/runtime.go +++ b/ignite/services/chain/runtime.go @@ -4,6 +4,9 @@ import ( "context" "os" "path/filepath" + "strconv" + "sync" + // "time" "github.com/nqd/flat" "github.com/pelletier/go-toml" @@ -58,6 +61,59 @@ func (c Chain) MultiNode(ctx context.Context, runner chaincmdrunner.Runner, args return err } +func (c Chain) StartMultiNode(ctx context.Context, runner chaincmdrunner.Runner, args MultiNodeArgs) error { + numVal, err := strconv.Atoi(args.NumValidator) + if err != nil { + return err + } + + // Kênh để nhận lỗi từ các goroutines + errCh := make(chan error, numVal) + var wg sync.WaitGroup + + // Tạo context có thể hủy bỏ + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + // Tạo các goroutines để chạy các node + for i := 0; i < numVal; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + home := filepath.Join(args.OutputDir, args.NodeDirPrefix+strconv.Itoa(i)) + + // Kiểm tra nếu context bị hủy + select { + case <-ctx.Done(): + return // Kết thúc goroutine nếu context bị hủy + default: + // Chạy node + errRunNode := runner.StartMultiNode(ctx, chaincmd.MultiNodeWithHome(home)) + + // Nếu có lỗi, gửi lỗi vào kênh và hủy toàn bộ goroutines + if errRunNode != nil { + errCh <- errRunNode + cancel() // Hủy tất cả goroutines khác + } + } + }(i) + } + + // Chờ tất cả các goroutines hoàn thành + wg.Wait() + + // Đóng kênh sau khi tất cả các goroutine hoàn tất + close(errCh) + + // Kiểm tra lỗi từ các goroutines + if len(errCh) > 0 { + // Lấy lỗi đầu tiên nếu có lỗi + return <-errCh + } + + return nil +} + // Start wraps the "appd start" command to begin running a chain from the daemon. func (c Chain) Start(ctx context.Context, runner chaincmdrunner.Runner, cfg *chainconfig.Config) error { validator, err := chainconfig.FirstValidator(cfg) diff --git a/ignite/services/chain/testnet.go b/ignite/services/chain/testnet.go index e60e94aa73..1e45abdea8 100644 --- a/ignite/services/chain/testnet.go +++ b/ignite/services/chain/testnet.go @@ -41,6 +41,7 @@ type MultiNodeArgs struct { OutputDir string NumValidator string ValidatorsStakeAmount string + NodeDirPrefix string } func (c Chain) TestnetMultiNode(ctx context.Context, args MultiNodeArgs) error { @@ -64,3 +65,25 @@ func (c Chain) TestnetMultiNode(ctx context.Context, args MultiNodeArgs) error { } return nil } + +func (c Chain) TestnetStartMultiNode(ctx context.Context, args MultiNodeArgs) error { + commands, err := c.Commands(ctx) + if err != nil { + return err + } + + // make sure that config.yml exists + if c.options.ConfigFile != "" { + if _, err := os.Stat(c.options.ConfigFile); err != nil { + return err + } + } else if _, err := chainconfig.LocateDefault(c.app.Path); err != nil { + return err + } + + err = c.StartMultiNode(ctx, commands, args) + if err != nil { + return err + } + return nil +} From d492d08ccc05f815f4e0c18c56be9fe58339ded8 Mon Sep 17 00:00:00 2001 From: Likes To Eat Fish Date: Sun, 29 Sep 2024 23:20:28 +0700 Subject: [PATCH 06/37] remove try --- ignite/cmd/testnet_multi_node.go | 2 - ignite/pkg/chaincmd/chaincmd.go | 6 --- ignite/pkg/chaincmd/in-place-testnet.go | 26 ------------ ignite/pkg/chaincmd/runner/chain.go | 13 ------ ignite/services/chain/runtime.go | 56 ------------------------- ignite/services/chain/testnet.go | 22 ---------- 6 files changed, 125 deletions(-) diff --git a/ignite/cmd/testnet_multi_node.go b/ignite/cmd/testnet_multi_node.go index 30c897bb5f..34bc89a8fa 100644 --- a/ignite/cmd/testnet_multi_node.go +++ b/ignite/cmd/testnet_multi_node.go @@ -1,7 +1,6 @@ package ignitecmd import ( - // "fmt" "math/rand" "strconv" "time" @@ -113,7 +112,6 @@ func testnetMultiNode(cmd *cobra.Command, session *cliui.Session) error { time.Sleep(7 * time.Second) - // c.TestnetStartMultiNode(cmd.Context(), args) m := cmdmodel.NewModel(c.Name(), cmd.Context(), args) _, err = tea.NewProgram(m).Run() return err diff --git a/ignite/pkg/chaincmd/chaincmd.go b/ignite/pkg/chaincmd/chaincmd.go index 057f4445d0..e7c9f78ab5 100644 --- a/ignite/pkg/chaincmd/chaincmd.go +++ b/ignite/pkg/chaincmd/chaincmd.go @@ -181,7 +181,6 @@ func (c ChainCmd) StartCommand(options ...string) step.Option { command := append([]string{ commandStart, }, options...) - fmt.Println(command) return c.daemonCommand(command) } @@ -644,11 +643,6 @@ func (c ChainCmd) daemonCommand(command []string) step.Option { return step.Exec(c.appCmd, c.attachHome(command)...) } -// daemonCommand returns the daemon command from the given command (including the home flag). -func (c ChainCmd) daemonCommandIncludedHomeFlag(command []string) step.Option { - return step.Exec(c.appCmd, command...) -} - // cliCommand returns the cli command from the provided command. func (c ChainCmd) cliCommand(command []string) step.Option { return step.Exec(c.appCmd, c.attachHome(command)...) diff --git a/ignite/pkg/chaincmd/in-place-testnet.go b/ignite/pkg/chaincmd/in-place-testnet.go index f4ecf360bb..8b33093a77 100644 --- a/ignite/pkg/chaincmd/in-place-testnet.go +++ b/ignite/pkg/chaincmd/in-place-testnet.go @@ -1,8 +1,6 @@ package chaincmd import ( - "fmt" - "github.com/ignite/cli/v29/ignite/pkg/cmdrunner/step" ) @@ -85,15 +83,6 @@ func MultiNodeWithValidatorsStakeAmount(satkeAmounts string) MultiNodeOption { } } -func MultiNodeWithHome(home string) MultiNodeOption { - return func(s []string) []string { - if len(home) > 0 { - return append(s, optionHome, home) - } - return s - } -} - // TestnetMultiNodeCommand return command to start testnet multinode. func (c ChainCmd) TestnetMultiNodeCommand(options ...MultiNodeOption) step.Option { command := []string{ @@ -104,21 +93,6 @@ func (c ChainCmd) TestnetMultiNodeCommand(options ...MultiNodeOption) step.Optio for _, apply := range options { command = apply(command) } - fmt.Println(command) return c.daemonCommand(command) } - -// TestnetMultiNodeCommand return command to start testnet multinode. -func (c ChainCmd) TestnetStartMultiNodeCommand(options ...MultiNodeOption) step.Option { - command := []string{ - commandStart, - } - - // Apply the options provided by the user - for _, apply := range options { - command = apply(command) - } - - return c.daemonCommandIncludedHomeFlag(command) -} diff --git a/ignite/pkg/chaincmd/runner/chain.go b/ignite/pkg/chaincmd/runner/chain.go index ad7e099419..ea39c315ac 100644 --- a/ignite/pkg/chaincmd/runner/chain.go +++ b/ignite/pkg/chaincmd/runner/chain.go @@ -69,19 +69,6 @@ func (r Runner) MultiNode(ctx context.Context, options ...chaincmd.MultiNodeOpti ) } -func (r Runner) StartMultiNode(ctx context.Context, options ...chaincmd.MultiNodeOption) error { - runOptions := runOptions{ - wrappedStdErrMaxLen: 50000, - stdout: os.Stdout, - stderr: os.Stderr, - } - return r.run( - ctx, - runOptions, - r.chainCmd.TestnetStartMultiNodeCommand(options...), - ) -} - // Gentx generates a genesis tx carrying a self delegation. func (r Runner) Gentx( ctx context.Context, diff --git a/ignite/services/chain/runtime.go b/ignite/services/chain/runtime.go index 739347df7c..b558eb2d7b 100644 --- a/ignite/services/chain/runtime.go +++ b/ignite/services/chain/runtime.go @@ -4,9 +4,6 @@ import ( "context" "os" "path/filepath" - "strconv" - "sync" - // "time" "github.com/nqd/flat" "github.com/pelletier/go-toml" @@ -61,59 +58,6 @@ func (c Chain) MultiNode(ctx context.Context, runner chaincmdrunner.Runner, args return err } -func (c Chain) StartMultiNode(ctx context.Context, runner chaincmdrunner.Runner, args MultiNodeArgs) error { - numVal, err := strconv.Atoi(args.NumValidator) - if err != nil { - return err - } - - // Kênh để nhận lỗi từ các goroutines - errCh := make(chan error, numVal) - var wg sync.WaitGroup - - // Tạo context có thể hủy bỏ - ctx, cancel := context.WithCancel(ctx) - defer cancel() - - // Tạo các goroutines để chạy các node - for i := 0; i < numVal; i++ { - wg.Add(1) - go func(i int) { - defer wg.Done() - home := filepath.Join(args.OutputDir, args.NodeDirPrefix+strconv.Itoa(i)) - - // Kiểm tra nếu context bị hủy - select { - case <-ctx.Done(): - return // Kết thúc goroutine nếu context bị hủy - default: - // Chạy node - errRunNode := runner.StartMultiNode(ctx, chaincmd.MultiNodeWithHome(home)) - - // Nếu có lỗi, gửi lỗi vào kênh và hủy toàn bộ goroutines - if errRunNode != nil { - errCh <- errRunNode - cancel() // Hủy tất cả goroutines khác - } - } - }(i) - } - - // Chờ tất cả các goroutines hoàn thành - wg.Wait() - - // Đóng kênh sau khi tất cả các goroutine hoàn tất - close(errCh) - - // Kiểm tra lỗi từ các goroutines - if len(errCh) > 0 { - // Lấy lỗi đầu tiên nếu có lỗi - return <-errCh - } - - return nil -} - // Start wraps the "appd start" command to begin running a chain from the daemon. func (c Chain) Start(ctx context.Context, runner chaincmdrunner.Runner, cfg *chainconfig.Config) error { validator, err := chainconfig.FirstValidator(cfg) diff --git a/ignite/services/chain/testnet.go b/ignite/services/chain/testnet.go index 1e45abdea8..cfd9d50137 100644 --- a/ignite/services/chain/testnet.go +++ b/ignite/services/chain/testnet.go @@ -65,25 +65,3 @@ func (c Chain) TestnetMultiNode(ctx context.Context, args MultiNodeArgs) error { } return nil } - -func (c Chain) TestnetStartMultiNode(ctx context.Context, args MultiNodeArgs) error { - commands, err := c.Commands(ctx) - if err != nil { - return err - } - - // make sure that config.yml exists - if c.options.ConfigFile != "" { - if _, err := os.Stat(c.options.ConfigFile); err != nil { - return err - } - } else if _, err := chainconfig.LocateDefault(c.app.Path); err != nil { - return err - } - - err = c.StartMultiNode(ctx, commands, args) - if err != nil { - return err - } - return nil -} From 1fdea3eb8d0f2f134e0897430ee796c4b8f6ddc6 Mon Sep 17 00:00:00 2001 From: Likes To Eat Fish Date: Sun, 29 Sep 2024 23:32:21 +0700 Subject: [PATCH 07/37] updates --- ignite/cmd/testnet_multi_node.go | 21 ++++++++++++++------- ignite/config/chain/base/config.go | 3 ++- ignite/templates/app/files/config.yml.plush | 3 ++- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/ignite/cmd/testnet_multi_node.go b/ignite/cmd/testnet_multi_node.go index 34bc89a8fa..1a9b4c0637 100644 --- a/ignite/cmd/testnet_multi_node.go +++ b/ignite/cmd/testnet_multi_node.go @@ -33,13 +33,21 @@ func NewTestnetMultiNode() *cobra.Command { stake: 200000000stake - name: validator4 stake: 200000000stake - or + output-dir: ./.testchain-testnet/ + chain-id: testchain-test-1 + node-dir-prefix: validator + + or random amount stake .... multi-node: random_validators: - count: 4 - min_stake: 50000000stake - max_stake: 150000000stake + count: 4 + min_stake: 50000000stake + max_stake: 150000000stake + output-dir: ./.testchain-testnet/ + chain-id: testchain-test-1 + node-dir-prefix: validator + `, Args: cobra.NoArgs, @@ -101,10 +109,9 @@ func testnetMultiNode(cmd *cobra.Command, session *cliui.Session) error { ValidatorsStakeAmount: amountDetails, OutputDir: cfg.MultiNode.OutputDir, NumValidator: strconv.Itoa(numVal), - NodeDirPrefix: "validator", //node + NodeDirPrefix: cfg.MultiNode.NodeDirPrefix, } - - //initialized 3 node directories + //initialized numVal node directories err = c.TestnetMultiNode(cmd.Context(), args) if err != nil { return err diff --git a/ignite/config/chain/base/config.go b/ignite/config/chain/base/config.go index 935e3b2674..b336ed27e3 100644 --- a/ignite/config/chain/base/config.go +++ b/ignite/config/chain/base/config.go @@ -162,7 +162,8 @@ type MultiNode struct { Validators []ValidatorDetails `yaml:"validators" doc:"List of manually configured validators."` RandomValidators RandomValidatorDetails `yaml:"random_validators" doc:"Configuration for randomly generated validators."` OutputDir string `yaml:"output-dir" doc:"Directory to store initialization data for the testnet"` - ChainID string `yaml:"chain-id" doc:"Directory to store initialization data for the testnet"` + ChainID string `yaml:"chain-id" doc:"Chain id for the testnet"` + NodeDirPrefix string `yaml:"node-dir-prefix" doc:"Node directory prefix for the testnet"` } // GetVersion returns the config version. diff --git a/ignite/templates/app/files/config.yml.plush b/ignite/templates/app/files/config.yml.plush index f2b28c4904..1805102ad0 100644 --- a/ignite/templates/app/files/config.yml.plush +++ b/ignite/templates/app/files/config.yml.plush @@ -33,4 +33,5 @@ multi-node: min_stake: 50000000stake max_stake: 150000000stake output-dir: ./.<%= AppName %>-testnet/ - chain-id: <%= AppName %>-test-1 \ No newline at end of file + chain-id: <%= AppName %>-test-1 + node-dir-prefix: validator \ No newline at end of file From 4a7c50d19efa21558970d0640f79bebab89940ab Mon Sep 17 00:00:00 2001 From: Likes To Eat Fish Date: Sun, 29 Sep 2024 23:40:11 +0700 Subject: [PATCH 08/37] updates --- ignite/cmd/model/testnet_multi_node.go | 2 +- ignite/cmd/testnet_multi_node.go | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/ignite/cmd/model/testnet_multi_node.go b/ignite/cmd/model/testnet_multi_node.go index d488e81885..21bbe64c87 100644 --- a/ignite/cmd/model/testnet_multi_node.go +++ b/ignite/cmd/model/testnet_multi_node.go @@ -152,7 +152,7 @@ func (m Model) View() string { return "[Stopped]" } - output := "Node Control:\n" + output := "Press keys 1,2,3.. to start and stop node 1,2,3.. respectively \nNode Control:\n" for i := 0; i < m.numNodes; i++ { output += fmt.Sprintf("%d. Node %d %s\n", i+1, i+1, statusText(m.nodeStatuses[i])) } diff --git a/ignite/cmd/testnet_multi_node.go b/ignite/cmd/testnet_multi_node.go index 1a9b4c0637..b0e10247a7 100644 --- a/ignite/cmd/testnet_multi_node.go +++ b/ignite/cmd/testnet_multi_node.go @@ -1,6 +1,7 @@ package ignitecmd import ( + "fmt" "math/rand" "strconv" "time" @@ -111,13 +112,15 @@ func testnetMultiNode(cmd *cobra.Command, session *cliui.Session) error { NumValidator: strconv.Itoa(numVal), NodeDirPrefix: cfg.MultiNode.NodeDirPrefix, } - //initialized numVal node directories + + fmt.Printf("Creating %s nodes \n\n", args.NumValidator) err = c.TestnetMultiNode(cmd.Context(), args) if err != nil { return err } time.Sleep(7 * time.Second) + fmt.Println() m := cmdmodel.NewModel(c.Name(), cmd.Context(), args) _, err = tea.NewProgram(m).Run() From 982e454d39ddb9d6bfb2d44c79f1ace031ff5aef Mon Sep 17 00:00:00 2001 From: Likes To Eat Fish Date: Mon, 30 Sep 2024 00:09:21 +0700 Subject: [PATCH 09/37] make format --- ignite/cmd/model/testnet_multi_node.go | 1 + ignite/cmd/testnet_multi_node.go | 5 +++-- ignite/pkg/chaincmd/in-place-testnet.go | 7 ++++--- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/ignite/cmd/model/testnet_multi_node.go b/ignite/cmd/model/testnet_multi_node.go index 21bbe64c87..8b98846d02 100644 --- a/ignite/cmd/model/testnet_multi_node.go +++ b/ignite/cmd/model/testnet_multi_node.go @@ -9,6 +9,7 @@ import ( "syscall" tea "github.com/charmbracelet/bubbletea" + "github.com/ignite/cli/v29/ignite/services/chain" ) diff --git a/ignite/cmd/testnet_multi_node.go b/ignite/cmd/testnet_multi_node.go index b0e10247a7..d66f293100 100644 --- a/ignite/cmd/testnet_multi_node.go +++ b/ignite/cmd/testnet_multi_node.go @@ -12,6 +12,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" tea "github.com/charmbracelet/bubbletea" + cmdmodel "github.com/ignite/cli/v29/ignite/cmd/model" "github.com/ignite/cli/v29/ignite/config/chain/base" "github.com/ignite/cli/v29/ignite/pkg/cliui" @@ -148,10 +149,10 @@ func getValidatorAmountStake(cfg base.MultiNode) (int, string, error) { stakeAmount := minS + rand.Uint64()%(maxS-minS+1) if amounts == "" { amounts = math.NewIntFromUint64(stakeAmount).String() - count += 1 + count++ } else { amounts = amounts + "," + math.NewIntFromUint64(stakeAmount).String() - count += 1 + count++ } } } else { diff --git a/ignite/pkg/chaincmd/in-place-testnet.go b/ignite/pkg/chaincmd/in-place-testnet.go index 8b33093a77..f3e9408a3e 100644 --- a/ignite/pkg/chaincmd/in-place-testnet.go +++ b/ignite/pkg/chaincmd/in-place-testnet.go @@ -48,10 +48,10 @@ func (c ChainCmd) TestnetInPlaceCommand(newChainID, newOperatorAddress string, o type MultiNodeOption func([]string) []string -func MultiNodeWithChainID(ChainId string) MultiNodeOption { +func MultiNodeWithChainID(chainId string) MultiNodeOption { return func(s []string) []string { - if len(ChainId) > 0 { - return append(s, optionChainID, ChainId) + if len(chainId) > 0 { + return append(s, optionChainID, chainId) } return s } @@ -74,6 +74,7 @@ func MultiNodeWithNumValidator(numVal string) MultiNodeOption { return s } } + func MultiNodeWithValidatorsStakeAmount(satkeAmounts string) MultiNodeOption { return func(s []string) []string { if len(satkeAmounts) > 0 { From 06f65b746f3bf5348cd6388c227926398c652940 Mon Sep 17 00:00:00 2001 From: Likes To Eat Fish Date: Mon, 30 Sep 2024 20:38:12 +0700 Subject: [PATCH 10/37] updates --- ignite/cmd/testnet_multi_node.go | 6 +++--- ignite/config/chain/base/config.go | 1 - ignite/services/chain/runtime.go | 1 + ignite/templates/app/files/config.yml.plush | 1 - 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/ignite/cmd/testnet_multi_node.go b/ignite/cmd/testnet_multi_node.go index d66f293100..73f7a4a57d 100644 --- a/ignite/cmd/testnet_multi_node.go +++ b/ignite/cmd/testnet_multi_node.go @@ -106,10 +106,11 @@ func testnetMultiNode(cmd *cobra.Command, session *cliui.Session) error { if err != nil { return err } + outputDir := ".ignite/local-chains/" + c.Name() + "d/testnet/" args := chain.MultiNodeArgs{ ChainID: cfg.MultiNode.ChainID, ValidatorsStakeAmount: amountDetails, - OutputDir: cfg.MultiNode.OutputDir, + OutputDir: outputDir, NumValidator: strconv.Itoa(numVal), NodeDirPrefix: cfg.MultiNode.NodeDirPrefix, } @@ -120,8 +121,7 @@ func testnetMultiNode(cmd *cobra.Command, session *cliui.Session) error { return err } - time.Sleep(7 * time.Second) - fmt.Println() + time.Sleep(2 * time.Second) m := cmdmodel.NewModel(c.Name(), cmd.Context(), args) _, err = tea.NewProgram(m).Run() diff --git a/ignite/config/chain/base/config.go b/ignite/config/chain/base/config.go index b336ed27e3..efe2e17b82 100644 --- a/ignite/config/chain/base/config.go +++ b/ignite/config/chain/base/config.go @@ -161,7 +161,6 @@ type RandomValidatorDetails struct { type MultiNode struct { Validators []ValidatorDetails `yaml:"validators" doc:"List of manually configured validators."` RandomValidators RandomValidatorDetails `yaml:"random_validators" doc:"Configuration for randomly generated validators."` - OutputDir string `yaml:"output-dir" doc:"Directory to store initialization data for the testnet"` ChainID string `yaml:"chain-id" doc:"Chain id for the testnet"` NodeDirPrefix string `yaml:"node-dir-prefix" doc:"Node directory prefix for the testnet"` } diff --git a/ignite/services/chain/runtime.go b/ignite/services/chain/runtime.go index b558eb2d7b..b8814474a4 100644 --- a/ignite/services/chain/runtime.go +++ b/ignite/services/chain/runtime.go @@ -48,6 +48,7 @@ func (c Chain) InPlace(ctx context.Context, runner chaincmdrunner.Runner, args I return err } +// MultiNode sets up multiple nodes in the chain network with the specified arguments and returns an error if any issue occurs. func (c Chain) MultiNode(ctx context.Context, runner chaincmdrunner.Runner, args MultiNodeArgs) error { err := runner.MultiNode(ctx, chaincmd.MultiNodeWithChainID(args.ChainID), diff --git a/ignite/templates/app/files/config.yml.plush b/ignite/templates/app/files/config.yml.plush index 1805102ad0..4a5fb380ba 100644 --- a/ignite/templates/app/files/config.yml.plush +++ b/ignite/templates/app/files/config.yml.plush @@ -32,6 +32,5 @@ multi-node: count: 3 min_stake: 50000000stake max_stake: 150000000stake - output-dir: ./.<%= AppName %>-testnet/ chain-id: <%= AppName %>-test-1 node-dir-prefix: validator \ No newline at end of file From 4f4bada6cccc0a2eadfc27d497a8fe501521c01c Mon Sep 17 00:00:00 2001 From: Likes To Eat Fish Date: Mon, 30 Sep 2024 23:30:23 +0700 Subject: [PATCH 11/37] minor --- .../cmd/testnet_multi_node.go.plush | 55 +++++++++++-------- 1 file changed, 31 insertions(+), 24 deletions(-) diff --git a/ignite/templates/app/files/cmd/{{binaryNamePrefix}}d/cmd/testnet_multi_node.go.plush b/ignite/templates/app/files/cmd/{{binaryNamePrefix}}d/cmd/testnet_multi_node.go.plush index 978d873a41..4691704a7d 100644 --- a/ignite/templates/app/files/cmd/{{binaryNamePrefix}}d/cmd/testnet_multi_node.go.plush +++ b/ignite/templates/app/files/cmd/{{binaryNamePrefix}}d/cmd/testnet_multi_node.go.plush @@ -5,20 +5,20 @@ import ( "encoding/json" "fmt" "io" - "strconv" - "strings" - + "math/rand" "os" "path/filepath" + "strconv" + "strings" + "time" cmtconfig "github.com/cometbft/cometbft/config" + types "github.com/cometbft/cometbft/types" + tmtime "github.com/cometbft/cometbft/types/time" "github.com/spf13/cobra" "github.com/spf13/pflag" "cosmossdk.io/math" - banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" - stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" - "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/flags" "github.com/cosmos/cosmos-sdk/client/tx" @@ -31,12 +31,11 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" "github.com/cosmos/cosmos-sdk/x/genutil" genutiltypes "github.com/cosmos/cosmos-sdk/x/genutil/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" - tmrand "github.com/cometbft/cometbft/libs/rand" - types "github.com/cometbft/cometbft/types" - tmtime "github.com/cometbft/cometbft/types/time" runtime "github.com/cosmos/cosmos-sdk/runtime" ) @@ -152,18 +151,18 @@ func initTestnetFiles( args initArgs, ) error { if args.chainID == "" { - args.chainID = "chain-" + tmrand.Str(6) + args.chainID = "chain-" + generateRandomString(6) } nodeIDs := make([]string, args.numValidators) valPubKeys := make([]cryptotypes.PubKey, args.numValidators) - simappConfig := srvconfig.DefaultConfig() - simappConfig.MinGasPrices = args.minGasPrices - simappConfig.API.Enable = false - simappConfig.BaseConfig.MinGasPrices = "0.0001" + sdk.DefaultBondDenom - simappConfig.Telemetry.EnableHostnameLabel = false - simappConfig.Telemetry.Enabled = false - simappConfig.Telemetry.PrometheusRetentionTime = 0 + appConfig := srvconfig.DefaultConfig() + appConfig.MinGasPrices = args.minGasPrices + appConfig.API.Enable = false + appConfig.BaseConfig.MinGasPrices = "0.0001" + sdk.DefaultBondDenom + appConfig.Telemetry.EnableHostnameLabel = false + appConfig.Telemetry.Enabled = false + appConfig.Telemetry.PrometheusRetentionTime = 0 var ( genAccounts []authtypes.GenesisAccount @@ -174,13 +173,9 @@ func initTestnetFiles( ) inBuf := bufio.NewReader(cmd.InOrStdin()) - // generate private keys, node IDs, and initial transactions for i := 0; i < args.numValidators; i++ { - // validator1 nodeDirName := fmt.Sprintf("%s%d", args.nodeDirPrefix, i) - // ../validator1 nodeDir := filepath.Join(args.outputDir, nodeDirName) - // ../validator1/config/gentx/ gentxsDir := filepath.Join(args.outputDir, nodeDirName, "config", "gentx") nodeConfig.SetRoot(nodeDir) @@ -294,9 +289,9 @@ func initTestnetFiles( return err } - simappConfig.GRPC.Address = args.startingIPAddress + ":" + strconv.Itoa(9090-2*i) - simappConfig.API.Address = "tcp://localhost:" + strconv.Itoa(1317-i) - srvconfig.WriteConfigFile(filepath.Join(nodeDir, "config", "app.toml"), simappConfig) + appConfig.GRPC.Address = args.startingIPAddress + ":" + strconv.Itoa(9090-2*i) + appConfig.API.Address = "tcp://localhost:" + strconv.Itoa(1317-i) + srvconfig.WriteConfigFile(filepath.Join(nodeDir, "config", "app.toml"), appConfig) } if err := initGenFiles(clientCtx, mbm, args.chainID, genAccounts, genBalances, genFiles, args.numValidators); err != nil { @@ -504,3 +499,15 @@ func isSubDir(src, dstDir string) (bool, error) { isInside := !strings.HasPrefix(relativePath, "..") && !filepath.IsAbs(relativePath) return isInside, nil } + +// generateRandomString generates a random string of the specified length. +func generateRandomString(length int) string { + const charset = "abcdefghijklmnopqrstuvwxyz0123456789" + var seededRand *rand.Rand = rand.New(rand.NewSource(time.Now().UnixNano())) + + b := make([]byte, length) + for i := range b { + b[i] = charset[seededRand.Intn(len(charset))] + } + return string(b) +} From 3a2f656d403fd20f15db4568734635d339774475 Mon Sep 17 00:00:00 2001 From: Likes To Eat Fish Date: Mon, 30 Sep 2024 23:55:15 +0700 Subject: [PATCH 12/37] rename and add info node --- ignite/cmd/model/testnet_multi_node.go | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/ignite/cmd/model/testnet_multi_node.go b/ignite/cmd/model/testnet_multi_node.go index 8b98846d02..cb08759815 100644 --- a/ignite/cmd/model/testnet_multi_node.go +++ b/ignite/cmd/model/testnet_multi_node.go @@ -20,7 +20,7 @@ const ( Running ) -type Model struct { +type MultiNode struct { appd string args chain.MultiNodeArgs ctx context.Context @@ -40,12 +40,12 @@ type UpdateStatusMsg struct { } // Initialize the model -func NewModel(chainname string, ctx context.Context, args chain.MultiNodeArgs) Model { +func NewModel(chainname string, ctx context.Context, args chain.MultiNodeArgs) MultiNode { numNodes, err := strconv.Atoi(args.NumValidator) if err != nil { panic(err) } - return Model{ + return MultiNode{ appd: chainname + "d", args: args, ctx: ctx, @@ -56,7 +56,7 @@ func NewModel(chainname string, ctx context.Context, args chain.MultiNodeArgs) M } // Implement the Update function -func (m Model) Init() tea.Cmd { +func (m MultiNode) Init() tea.Cmd { return nil } @@ -105,7 +105,7 @@ func RunNode(nodeIdx int, start bool, pid *int, args chain.MultiNodeArgs, appd s } // Stop all nodes -func (m *Model) StopAllNodes() { +func (m *MultiNode) StopAllNodes() { for i := 0; i < m.numNodes; i++ { if m.nodeStatuses[i] == Running { RunNode(i, false, &m.pids[i], m.args, m.appd)() // Stop node @@ -114,7 +114,7 @@ func (m *Model) StopAllNodes() { } // Update handles messages and updates the model -func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (m MultiNode) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: switch msg.String() { @@ -145,7 +145,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } // View renders the interface -func (m Model) View() string { +func (m MultiNode) View() string { statusText := func(status NodeStatus) string { if status == Running { return "[Running]" @@ -153,9 +153,16 @@ func (m Model) View() string { return "[Stopped]" } + infoNode := func(i int) string { + chainId := m.args.ChainID + home := m.args.OutputDir + ipaddr := "tcp://127.0.0.1:" + strconv.Itoa(26657-3*i) + return fmt.Sprintf("INFO: ChainID:%s | Home:%s | Node:%s ", chainId, home, ipaddr) + } + output := "Press keys 1,2,3.. to start and stop node 1,2,3.. respectively \nNode Control:\n" for i := 0; i < m.numNodes; i++ { - output += fmt.Sprintf("%d. Node %d %s\n", i+1, i+1, statusText(m.nodeStatuses[i])) + output += fmt.Sprintf("%d. Node %d %s -- %s\n", i+1, i+1, statusText(m.nodeStatuses[i]), infoNode(i)) } output += "Press q to quit.\n" return output From fa09a759567d8eb441fb50ffe486ad58a67ec3ba Mon Sep 17 00:00:00 2001 From: Likes To Eat Fish Date: Tue, 1 Oct 2024 00:39:14 +0700 Subject: [PATCH 13/37] add a reset command --- ignite/cmd/testnet_multi_node.go | 24 +++++++++++++++++++----- ignite/services/chain/testnet.go | 2 ++ 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/ignite/cmd/testnet_multi_node.go b/ignite/cmd/testnet_multi_node.go index 73f7a4a57d..de44149130 100644 --- a/ignite/cmd/testnet_multi_node.go +++ b/ignite/cmd/testnet_multi_node.go @@ -1,18 +1,18 @@ package ignitecmd import ( - "fmt" "math/rand" + "os" + "path/filepath" "strconv" "time" "cosmossdk.io/math" + tea "github.com/charmbracelet/bubbletea" "github.com/spf13/cobra" sdk "github.com/cosmos/cosmos-sdk/types" - tea "github.com/charmbracelet/bubbletea" - cmdmodel "github.com/ignite/cli/v29/ignite/cmd/model" "github.com/ignite/cli/v29/ignite/config/chain/base" "github.com/ignite/cli/v29/ignite/pkg/cliui" @@ -61,6 +61,7 @@ func NewTestnetMultiNode() *cobra.Command { c.Flags().AddFlagSet(flagSetCheckDependencies()) c.Flags().AddFlagSet(flagSetSkipProto()) c.Flags().AddFlagSet(flagSetVerbose()) + c.Flags().BoolP(flagResetOnce, "r", false, "reset the app state once on init") c.Flags().Bool(flagQuitOnFail, false, "quit program if the app fails to start") return c @@ -106,7 +107,12 @@ func testnetMultiNode(cmd *cobra.Command, session *cliui.Session) error { if err != nil { return err } - outputDir := ".ignite/local-chains/" + c.Name() + "d/testnet/" + homeDir, err := os.UserHomeDir() + if err != nil { + return err + } + + outputDir := filepath.Join(homeDir, ".ignite/local-chains/"+c.Name()+"d/testnet/") args := chain.MultiNodeArgs{ ChainID: cfg.MultiNode.ChainID, ValidatorsStakeAmount: amountDetails, @@ -115,7 +121,15 @@ func testnetMultiNode(cmd *cobra.Command, session *cliui.Session) error { NodeDirPrefix: cfg.MultiNode.NodeDirPrefix, } - fmt.Printf("Creating %s nodes \n\n", args.NumValidator) + resetOnce, _ := cmd.Flags().GetBool(flagResetOnce) + if resetOnce { + // If resetOnce is true, the app state will be reset by deleting the output directory. + err := os.RemoveAll(outputDir) + if err != nil { + return err + } + } + err = c.TestnetMultiNode(cmd.Context(), args) if err != nil { return err diff --git a/ignite/services/chain/testnet.go b/ignite/services/chain/testnet.go index cfd9d50137..3941be0014 100644 --- a/ignite/services/chain/testnet.go +++ b/ignite/services/chain/testnet.go @@ -44,6 +44,8 @@ type MultiNodeArgs struct { NodeDirPrefix string } +// If the app state still exists, TestnetMultiNode will reuse it. +// Otherwise, it will automatically re-initialize the app state from the beginning. func (c Chain) TestnetMultiNode(ctx context.Context, args MultiNodeArgs) error { commands, err := c.Commands(ctx) if err != nil { From 1daa0c35a932358c1d40fb64db1d8ef2d1cb750f Mon Sep 17 00:00:00 2001 From: Likes To Eat Fish Date: Tue, 1 Oct 2024 11:23:45 +0700 Subject: [PATCH 14/37] changelog --- changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.md b/changelog.md index ab39e0560f..cb1b45f9d6 100644 --- a/changelog.md +++ b/changelog.md @@ -19,6 +19,7 @@ - [#4300](https://github.com/ignite/cli/pull/4300) Only panics the module in the most top function level - [#4327](https://github.com/ignite/cli/pull/4327) Use the TxConfig from simState instead create a new one - [#4326](https://github.com/ignite/cli/pull/4326) fAdd `buf.build` version to `ignite version` command +- [#4377](https://github.com/ignite/cli/pull/4377) Add multi node (validator) testnet. ### Changes From dedd592279faaa9c4ae4a76c9258fb3b9fc270e8 Mon Sep 17 00:00:00 2001 From: Likes To Eat Fish Date: Tue, 1 Oct 2024 11:45:41 +0700 Subject: [PATCH 15/37] lint --- ignite/cmd/model/testnet_multi_node.go | 49 +++++++++++---------- ignite/cmd/testnet_multi_node.go | 13 +++--- ignite/config/chain/v1/config.go | 7 ++- ignite/pkg/chaincmd/in-place-testnet.go | 6 +-- ignite/pkg/markdownviewer/markdownviewer.go | 1 + 5 files changed, 43 insertions(+), 33 deletions(-) diff --git a/ignite/cmd/model/testnet_multi_node.go b/ignite/cmd/model/testnet_multi_node.go index cb08759815..e26dba93aa 100644 --- a/ignite/cmd/model/testnet_multi_node.go +++ b/ignite/cmd/model/testnet_multi_node.go @@ -21,9 +21,9 @@ const ( ) type MultiNode struct { + ctx context.Context appd string args chain.MultiNodeArgs - ctx context.Context nodeStatuses []NodeStatus pids []int // Store the PIDs of the running processes @@ -39,35 +39,35 @@ type UpdateStatusMsg struct { status NodeStatus } -// Initialize the model -func NewModel(chainname string, ctx context.Context, args chain.MultiNodeArgs) MultiNode { +// Initialize the model. +func NewModel(ctx context.Context, chainname string, args chain.MultiNodeArgs) MultiNode { numNodes, err := strconv.Atoi(args.NumValidator) if err != nil { panic(err) } return MultiNode{ + ctx: ctx, appd: chainname + "d", args: args, - ctx: ctx, nodeStatuses: make([]NodeStatus, numNodes), // initial states of nodes pids: make([]int, numNodes), numNodes: numNodes, } } -// Implement the Update function +// Implement the Update function. func (m MultiNode) Init() tea.Cmd { return nil } -// ToggleNode toggles the state of a node +// ToggleNode toggles the state of a node. func ToggleNode(nodeIdx int) tea.Cmd { return func() tea.Msg { return ToggleNodeMsg{nodeIdx: nodeIdx} } } -// Run or stop the node based on its status +// Run or stop the node based on its status. func RunNode(nodeIdx int, start bool, pid *int, args chain.MultiNodeArgs, appd string) tea.Cmd { return func() tea.Msg { if start { @@ -87,24 +87,27 @@ func RunNode(nodeIdx int, start bool, pid *int, args chain.MultiNodeArgs, appd s } *pid = cmd.Process.Pid // Store the PID - go cmd.Wait() // Let the process run asynchronously - return UpdateStatusMsg{nodeIdx: nodeIdx, status: Running} - } else { - // Use kill to stop the node process by PID - if *pid != 0 { - err := syscall.Kill(-*pid, syscall.SIGTERM) // Stop the daemon process - if err != nil { - fmt.Printf("Failed to stop node %d: %v\n", nodeIdx+1, err) - } else { - *pid = 0 // Reset PID after stopping + go func() { + if err := cmd.Wait(); err != nil { + fmt.Printf("Node %d exited with error: %v\n", nodeIdx+1, err) } + }() + return UpdateStatusMsg{nodeIdx: nodeIdx, status: Running} + } + // Use kill to stop the node process by PID + if *pid != 0 { + err := syscall.Kill(-*pid, syscall.SIGTERM) // Stop the daemon process + if err != nil { + fmt.Printf("Failed to stop node %d: %v\n", nodeIdx+1, err) + } else { + *pid = 0 // Reset PID after stopping } - return UpdateStatusMsg{nodeIdx: nodeIdx, status: Stopped} } + return UpdateStatusMsg{nodeIdx: nodeIdx, status: Stopped} } } -// Stop all nodes +// Stop all nodes. func (m *MultiNode) StopAllNodes() { for i := 0; i < m.numNodes; i++ { if m.nodeStatuses[i] == Running { @@ -113,7 +116,7 @@ func (m *MultiNode) StopAllNodes() { } } -// Update handles messages and updates the model +// Update handles messages and updates the model. func (m MultiNode) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: @@ -144,7 +147,7 @@ func (m MultiNode) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil } -// View renders the interface +// View renders the interface. func (m MultiNode) View() string { statusText := func(status NodeStatus) string { if status == Running { @@ -154,10 +157,10 @@ func (m MultiNode) View() string { } infoNode := func(i int) string { - chainId := m.args.ChainID + chainID := m.args.ChainID home := m.args.OutputDir ipaddr := "tcp://127.0.0.1:" + strconv.Itoa(26657-3*i) - return fmt.Sprintf("INFO: ChainID:%s | Home:%s | Node:%s ", chainId, home, ipaddr) + return fmt.Sprintf("INFO: ChainID:%s | Home:%s | Node:%s ", chainID, home, ipaddr) } output := "Press keys 1,2,3.. to start and stop node 1,2,3.. respectively \nNode Control:\n" diff --git a/ignite/cmd/testnet_multi_node.go b/ignite/cmd/testnet_multi_node.go index de44149130..8396584a48 100644 --- a/ignite/cmd/testnet_multi_node.go +++ b/ignite/cmd/testnet_multi_node.go @@ -7,10 +7,11 @@ import ( "strconv" "time" - "cosmossdk.io/math" tea "github.com/charmbracelet/bubbletea" "github.com/spf13/cobra" + "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" cmdmodel "github.com/ignite/cli/v29/ignite/cmd/model" @@ -137,12 +138,12 @@ func testnetMultiNode(cmd *cobra.Command, session *cliui.Session) error { time.Sleep(2 * time.Second) - m := cmdmodel.NewModel(c.Name(), cmd.Context(), args) + m := cmdmodel.NewModel(cmd.Context(), c.Name(), args) _, err = tea.NewProgram(m).Run() return err } -// getValidatorAmountStake returns the number of validators and the amountStakes arg from config.MultiNode +// getValidatorAmountStake returns the number of validators and the amountStakes arg from config.MultiNode. func getValidatorAmountStake(cfg base.MultiNode) (int, string, error) { var amounts string count := 0 @@ -160,7 +161,7 @@ func getValidatorAmountStake(cfg base.MultiNode) (int, string, error) { minS := minStake.Amount.Uint64() maxS := maxStake.Amount.Uint64() for i := 0; i < numVal; i++ { - stakeAmount := minS + rand.Uint64()%(maxS-minS+1) + stakeAmount := minS + rand.Uint64()%(maxS-minS+1) // #nosec G404 if amounts == "" { amounts = math.NewIntFromUint64(stakeAmount).String() count++ @@ -177,10 +178,10 @@ func getValidatorAmountStake(cfg base.MultiNode) (int, string, error) { } if amounts == "" { amounts = stakeAmount.Amount.String() - count += 1 + count++ } else { amounts = amounts + "," + stakeAmount.Amount.String() - count += 1 + count++ } } } diff --git a/ignite/config/chain/v1/config.go b/ignite/config/chain/v1/config.go index 2960d4b826..350f0c0426 100644 --- a/ignite/config/chain/v1/config.go +++ b/ignite/config/chain/v1/config.go @@ -1,6 +1,7 @@ package v1 import ( + "fmt" "io" "github.com/imdario/mergo" @@ -59,8 +60,12 @@ func (c *Config) updateValidatorAddresses() (err error) { if err != nil { return err } + portIncrement := margin * i + if portIncrement < 0 { + return fmt.Errorf("calculated port increment is negative: %d", portIncrement) //nolint: forbidigo + } - servers, err = incrementDefaultServerPortsBy(servers, uint64(margin*i)) + servers, err = incrementDefaultServerPortsBy(servers, uint64(portIncrement)) if err != nil { return err } diff --git a/ignite/pkg/chaincmd/in-place-testnet.go b/ignite/pkg/chaincmd/in-place-testnet.go index f3e9408a3e..fd87f7bcda 100644 --- a/ignite/pkg/chaincmd/in-place-testnet.go +++ b/ignite/pkg/chaincmd/in-place-testnet.go @@ -48,10 +48,10 @@ func (c ChainCmd) TestnetInPlaceCommand(newChainID, newOperatorAddress string, o type MultiNodeOption func([]string) []string -func MultiNodeWithChainID(chainId string) MultiNodeOption { +func MultiNodeWithChainID(chainID string) MultiNodeOption { return func(s []string) []string { - if len(chainId) > 0 { - return append(s, optionChainID, chainId) + if len(chainID) > 0 { + return append(s, optionChainID, chainID) } return s } diff --git a/ignite/pkg/markdownviewer/markdownviewer.go b/ignite/pkg/markdownviewer/markdownviewer.go index 04572280f6..d393ce36f0 100644 --- a/ignite/pkg/markdownviewer/markdownviewer.go +++ b/ignite/pkg/markdownviewer/markdownviewer.go @@ -31,6 +31,7 @@ func config(path string) (ui.Config, error) { if err != nil { return ui.Config{}, err } + //nolint: gosec width = uint(w) if width > 120 { width = 120 From c2fb62326e504bd612a4cf60f22e29cf8230cf14 Mon Sep 17 00:00:00 2001 From: Likes To Eat Fish Date: Wed, 2 Oct 2024 23:26:45 +0700 Subject: [PATCH 16/37] use config.yml with more validators --- ignite/cmd/model/testnet_multi_node.go | 3 +- ignite/cmd/testnet_multi_node.go | 64 +++++++-------------- ignite/config/chain/base/config.go | 22 ------- ignite/pkg/chaincmd/chaincmd.go | 1 + ignite/pkg/chaincmd/in-place-testnet.go | 9 +++ ignite/services/chain/runtime.go | 2 +- ignite/services/chain/testnet.go | 1 - ignite/templates/app/files/config.yml.plush | 20 ++----- 8 files changed, 38 insertions(+), 84 deletions(-) diff --git a/ignite/cmd/model/testnet_multi_node.go b/ignite/cmd/model/testnet_multi_node.go index e26dba93aa..99108cf3ef 100644 --- a/ignite/cmd/model/testnet_multi_node.go +++ b/ignite/cmd/model/testnet_multi_node.go @@ -157,10 +157,9 @@ func (m MultiNode) View() string { } infoNode := func(i int) string { - chainID := m.args.ChainID home := m.args.OutputDir ipaddr := "tcp://127.0.0.1:" + strconv.Itoa(26657-3*i) - return fmt.Sprintf("INFO: ChainID:%s | Home:%s | Node:%s ", chainID, home, ipaddr) + return fmt.Sprintf("INFO:| Home:%s | Node:%s ", home, ipaddr) } output := "Press keys 1,2,3.. to start and stop node 1,2,3.. respectively \nNode Control:\n" diff --git a/ignite/cmd/testnet_multi_node.go b/ignite/cmd/testnet_multi_node.go index 8396584a48..e93736f3e9 100644 --- a/ignite/cmd/testnet_multi_node.go +++ b/ignite/cmd/testnet_multi_node.go @@ -1,7 +1,6 @@ package ignitecmd import ( - "math/rand" "os" "path/filepath" "strconv" @@ -10,16 +9,18 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/spf13/cobra" - "cosmossdk.io/math" - sdk "github.com/cosmos/cosmos-sdk/types" cmdmodel "github.com/ignite/cli/v29/ignite/cmd/model" - "github.com/ignite/cli/v29/ignite/config/chain/base" + "github.com/ignite/cli/v29/ignite/config/chain/v1" "github.com/ignite/cli/v29/ignite/pkg/cliui" "github.com/ignite/cli/v29/ignite/services/chain" ) +const ( + flagNodeDirPrefix = "node-dir-prefix" +) + func NewTestnetMultiNode() *cobra.Command { c := &cobra.Command{ Use: "multi-node", @@ -63,6 +64,7 @@ func NewTestnetMultiNode() *cobra.Command { c.Flags().AddFlagSet(flagSetSkipProto()) c.Flags().AddFlagSet(flagSetVerbose()) c.Flags().BoolP(flagResetOnce, "r", false, "reset the app state once on init") + c.Flags().String(flagNodeDirPrefix, "validator", "prefix of dir node") c.Flags().Bool(flagQuitOnFail, false, "quit program if the app fails to start") return c @@ -104,7 +106,7 @@ func testnetMultiNode(cmd *cobra.Command, session *cliui.Session) error { return err } - numVal, amountDetails, err := getValidatorAmountStake(cfg.MultiNode) + numVal, amountDetails, err := getValidatorAmountStake(cfg.Validators) if err != nil { return err } @@ -112,14 +114,14 @@ func testnetMultiNode(cmd *cobra.Command, session *cliui.Session) error { if err != nil { return err } + nodeDirPrefix, _ := cmd.Flags().GetString(flagNodeDirPrefix) outputDir := filepath.Join(homeDir, ".ignite/local-chains/"+c.Name()+"d/testnet/") args := chain.MultiNodeArgs{ - ChainID: cfg.MultiNode.ChainID, - ValidatorsStakeAmount: amountDetails, OutputDir: outputDir, NumValidator: strconv.Itoa(numVal), - NodeDirPrefix: cfg.MultiNode.NodeDirPrefix, + ValidatorsStakeAmount: amountDetails, + NodeDirPrefix: nodeDirPrefix, } resetOnce, _ := cmd.Flags().GetBool(flagResetOnce) @@ -144,47 +146,21 @@ func testnetMultiNode(cmd *cobra.Command, session *cliui.Session) error { } // getValidatorAmountStake returns the number of validators and the amountStakes arg from config.MultiNode. -func getValidatorAmountStake(cfg base.MultiNode) (int, string, error) { +func getValidatorAmountStake(validators []v1.Validator) (int, string, error) { + numVal := len(validators) var amounts string - count := 0 - if len(cfg.Validators) == 0 { - numVal := cfg.RandomValidators.Count - minStake, err := sdk.ParseCoinNormalized(cfg.RandomValidators.MinStake) - if err != nil { - return count, amounts, err - } - maxStake, err := sdk.ParseCoinNormalized(cfg.RandomValidators.MaxStake) + for _, v := range validators { + stakeAmount, err := sdk.ParseCoinNormalized(v.Bonded) if err != nil { - return count, amounts, err - } - minS := minStake.Amount.Uint64() - maxS := maxStake.Amount.Uint64() - for i := 0; i < numVal; i++ { - stakeAmount := minS + rand.Uint64()%(maxS-minS+1) // #nosec G404 - if amounts == "" { - amounts = math.NewIntFromUint64(stakeAmount).String() - count++ - } else { - amounts = amounts + "," + math.NewIntFromUint64(stakeAmount).String() - count++ - } + return numVal, amounts, err } - } else { - for _, v := range cfg.Validators { - stakeAmount, err := sdk.ParseCoinNormalized(v.Stake) - if err != nil { - return count, amounts, err - } - if amounts == "" { - amounts = stakeAmount.Amount.String() - count++ - } else { - amounts = amounts + "," + stakeAmount.Amount.String() - count++ - } + if amounts == "" { + amounts = stakeAmount.Amount.String() + } else { + amounts = amounts + "," + stakeAmount.Amount.String() } } - return count, amounts, nil + return numVal, amounts, nil } diff --git a/ignite/config/chain/base/config.go b/ignite/config/chain/base/config.go index efe2e17b82..a9e6340e63 100644 --- a/ignite/config/chain/base/config.go +++ b/ignite/config/chain/base/config.go @@ -141,28 +141,6 @@ type Config struct { Client Client `yaml:"client,omitempty" doc:"Configures client code generation."` Genesis xyaml.Map `yaml:"genesis,omitempty" doc:"Custom genesis block modifications. Follow the nesting of the genesis file here to access all the parameters."` Minimal bool `yaml:"minimal,omitempty" doc:"Indicates if the blockchain is minimal with the required Cosmos SDK modules."` - MultiNode MultiNode `yaml:"multi-node" doc:"Configuration for testnet multi node."` -} - -// Validator defines the configuration for a single validator. -type ValidatorDetails struct { - Name string `yaml:"name" doc:"Name of the validator."` - Stake string `yaml:"stake" doc:"Amount of stake associated with the validator."` -} - -// RandomValidator defines the configuration for random validators. -type RandomValidatorDetails struct { - Count int `yaml:"count" doc:"Number of random validators to be generated."` - MinStake string `yaml:"min_stake" doc:"Minimum stake for each random validator."` - MaxStake string `yaml:"max_stake" doc:"Maximum stake for each random validator."` -} - -// MultiNode holds the configuration related to multiple validators and random validators. -type MultiNode struct { - Validators []ValidatorDetails `yaml:"validators" doc:"List of manually configured validators."` - RandomValidators RandomValidatorDetails `yaml:"random_validators" doc:"Configuration for randomly generated validators."` - ChainID string `yaml:"chain-id" doc:"Chain id for the testnet"` - NodeDirPrefix string `yaml:"node-dir-prefix" doc:"Node directory prefix for the testnet"` } // GetVersion returns the config version. diff --git a/ignite/pkg/chaincmd/chaincmd.go b/ignite/pkg/chaincmd/chaincmd.go index e7c9f78ab5..c8785f63d6 100644 --- a/ignite/pkg/chaincmd/chaincmd.go +++ b/ignite/pkg/chaincmd/chaincmd.go @@ -63,6 +63,7 @@ const ( optionAmountStakes = "--validators-stake-amount" optionOutPutDir = "--output-dir" optionNumValidator = "--v" + optionNodeDirPrefix = "--node-dir-prefix" constTendermint = "tendermint" constJSON = "json" diff --git a/ignite/pkg/chaincmd/in-place-testnet.go b/ignite/pkg/chaincmd/in-place-testnet.go index fd87f7bcda..cb8c7eb545 100644 --- a/ignite/pkg/chaincmd/in-place-testnet.go +++ b/ignite/pkg/chaincmd/in-place-testnet.go @@ -84,6 +84,15 @@ func MultiNodeWithValidatorsStakeAmount(satkeAmounts string) MultiNodeOption { } } +func MultiNodeDirPrefix(nodeDirPrefix string) MultiNodeOption { + return func(s []string) []string { + if len(nodeDirPrefix) > 0 { + return append(s, optionNodeDirPrefix, nodeDirPrefix) + } + return s + } +} + // TestnetMultiNodeCommand return command to start testnet multinode. func (c ChainCmd) TestnetMultiNodeCommand(options ...MultiNodeOption) step.Option { command := []string{ diff --git a/ignite/services/chain/runtime.go b/ignite/services/chain/runtime.go index b8814474a4..63bf1e2db7 100644 --- a/ignite/services/chain/runtime.go +++ b/ignite/services/chain/runtime.go @@ -51,10 +51,10 @@ func (c Chain) InPlace(ctx context.Context, runner chaincmdrunner.Runner, args I // MultiNode sets up multiple nodes in the chain network with the specified arguments and returns an error if any issue occurs. func (c Chain) MultiNode(ctx context.Context, runner chaincmdrunner.Runner, args MultiNodeArgs) error { err := runner.MultiNode(ctx, - chaincmd.MultiNodeWithChainID(args.ChainID), chaincmd.MultiNodeWithDirOutput(args.OutputDir), chaincmd.MultiNodeWithNumValidator(args.NumValidator), chaincmd.MultiNodeWithValidatorsStakeAmount(args.ValidatorsStakeAmount), + chaincmd.MultiNodeDirPrefix(args.NodeDirPrefix), ) return err } diff --git a/ignite/services/chain/testnet.go b/ignite/services/chain/testnet.go index 3941be0014..f1ac388819 100644 --- a/ignite/services/chain/testnet.go +++ b/ignite/services/chain/testnet.go @@ -37,7 +37,6 @@ func (c Chain) TestnetInPlace(ctx context.Context, args InPlaceArgs) error { } type MultiNodeArgs struct { - ChainID string OutputDir string NumValidator string ValidatorsStakeAmount string diff --git a/ignite/templates/app/files/config.yml.plush b/ignite/templates/app/files/config.yml.plush index 4a5fb380ba..e6c00d4729 100644 --- a/ignite/templates/app/files/config.yml.plush +++ b/ignite/templates/app/files/config.yml.plush @@ -20,17 +20,9 @@ faucet: validators: - name: alice bonded: 100000000stake -multi-node: - validators: - - name: validator1 - stake: 100000000stake - - name: validator2 - stake: 200000000stake - - name: validator3 - stake: 300000000stake - random_validators: - count: 3 - min_stake: 50000000stake - max_stake: 150000000stake - chain-id: <%= AppName %>-test-1 - node-dir-prefix: validator \ No newline at end of file +- name: validator1 + bonded: 100000000stake +- name: validator2 + bonded: 200000000stake +- name: validator3 + bonded: 300000000stake \ No newline at end of file From 05b7822fec849308a31d5587b129e9c750c69e2c Mon Sep 17 00:00:00 2001 From: Likes To Eat Fish Date: Thu, 3 Oct 2024 00:19:57 +0700 Subject: [PATCH 17/37] updates docs --- docs/docs/03-CLI-Commands/01-cli-commands.md | 56 +++++++++++++++++++- ignite/cmd/testnet_multi_node.go | 46 +++++++--------- 2 files changed, 75 insertions(+), 27 deletions(-) diff --git a/docs/docs/03-CLI-Commands/01-cli-commands.md b/docs/docs/03-CLI-Commands/01-cli-commands.md index f029a32fad..48e5d30a64 100644 --- a/docs/docs/03-CLI-Commands/01-cli-commands.md +++ b/docs/docs/03-CLI-Commands/01-cli-commands.md @@ -3666,7 +3666,7 @@ Start a testnet local **Synopsis** -The commands in this namespace allow you to start your local testnet for development purposes. Currently there is only one feature to create a testnet from any state network (including mainnet). +The commands in this namespace allow you to start your local testnet for development purposes. The "in-place" command is used to create and start a testnet from current local net state(including mainnet). @@ -3675,9 +3675,12 @@ We can create a testnet from the local network state and mint additional coins f During development, in-place allows you to quickly reboot the chain from a multi-node network state to a node you have full control over. +The "multi-node" initialization and start command is used to set up and launch a multi-node network, allowing you to enable, disable, and providing full interaction capabilities with the chain. The stake amount for each validator is defined in the config.yml file. + **SEE ALSO** * [ignite testnet in-place](#ignite-testnet-in-place) - Create and start a testnet from current local net state +* [ignite testnet multi-node](#ignite-testnet-multi-node) - Initialize and provide multi-node on/off functionality ## ignite testnet in-place @@ -3725,6 +3728,57 @@ ignite chain debug [flags] -c, --config string path to Ignite config file (default: ./config.yml) ``` +## ignite testnet multi-node + +Initialize and start multiple nodes + +**Synopsis** + +The "multi-node" command allows developers to easily set up, initialize, and manage multiple nodes for a testnet environment. This command provides full flexibility in enabling or disabling each node as desired, making it a powerful tool for simulating a multi-node blockchain network during development. + +By using the config.yml file, you can define validators with custom bonded amounts, giving you control over how each node participates in the network: + +``` + validators: + - name: alice + bonded: 100000000stake + - name: validator1 + bonded: 100000000stake + - name: validator2 + bonded: 200000000stake + - name: validator3 + bonded: 300000000stake + +``` + +Each validator's bonded stake can be adjusted according to your testing needs, providing a realistic environment to simulate various scenarios. + +The multi-node command not only initializes these nodes but also gives you control over starting, stopping individual nodes. This level of control ensures you can test and iterate rapidly without needing to reinitialize the entire network each time a change is made. This makes it ideal for experimenting with validator behavior, network dynamics, and the impact of various configurations. + +All initialized nodes will be stored under the `.ignite/local-chains//testnet/` directory, which allows easy access and management. + + +Usage + +``` +ignite testnet multi-node [flags] +``` + +**Options** + +``` + -r, --reset-once reset the app state once on init + --node-dir-prefix dir prefix for node (default "validator") + -h, --help help for debug + -p, --path string path of the app (default ".") +``` + +**Options inherited from parent commands** + +``` + -c, --config string path to Ignite config file (default: ./config.yml) +``` + **SEE ALSO** * [ignite](#ignite) - Ignite CLI offers everything you need to scaffold, test, build, start testnet and launch your blockchain \ No newline at end of file diff --git a/ignite/cmd/testnet_multi_node.go b/ignite/cmd/testnet_multi_node.go index e93736f3e9..ff4c9e9518 100644 --- a/ignite/cmd/testnet_multi_node.go +++ b/ignite/cmd/testnet_multi_node.go @@ -24,35 +24,29 @@ const ( func NewTestnetMultiNode() *cobra.Command { c := &cobra.Command{ Use: "multi-node", - Short: "Create a network test multi node", - Long: `Create a test network with the number of nodes from the config.yml file: + Short: "Initialize and provide multi-node on/off functionality", + Long: `Initialize the test network with the number of nodes and bonded from the config.yml file:: ... - multi-node: - validators: - - name: validator1 - stake: 100000000stake - - name: validator2 - stake: 200000000stake - - name: validator3 - stake: 200000000stake - - name: validator4 - stake: 200000000stake - output-dir: ./.testchain-testnet/ - chain-id: testchain-test-1 - node-dir-prefix: validator - - or random amount stake - .... - multi-node: - random_validators: - count: 4 - min_stake: 50000000stake - max_stake: 150000000stake - output-dir: ./.testchain-testnet/ - chain-id: testchain-test-1 - node-dir-prefix: validator + validators: + - name: alice + bonded: 100000000stake + - name: validator1 + bonded: 100000000stake + - name: validator2 + bonded: 200000000stake + - name: validator3 + bonded: 300000000stake + The "multi-node" command allows developers to easily set up, initialize, and manage multiple nodes for a + testnet environment. This command provides full flexibility in enabling or disabling each node as desired, + making it a powerful tool for simulating a multi-node blockchain network during development. + + Usage: + ignite testnet multi-node [flags] + + + `, Args: cobra.NoArgs, RunE: testnetMultiNodeHandler, From 5b8b7f70e0d8b6c598f6575b4fe9172d0598842d Mon Sep 17 00:00:00 2001 From: Likes To Eat Fish Date: Sat, 5 Oct 2024 13:09:52 +0700 Subject: [PATCH 18/37] show log --- ignite/cmd/model/testnet_multi_node.go | 77 ++++++++++++++++++-------- 1 file changed, 53 insertions(+), 24 deletions(-) diff --git a/ignite/cmd/model/testnet_multi_node.go b/ignite/cmd/model/testnet_multi_node.go index 99108cf3ef..d383fef8ba 100644 --- a/ignite/cmd/model/testnet_multi_node.go +++ b/ignite/cmd/model/testnet_multi_node.go @@ -1,6 +1,7 @@ package cmdmodel import ( + "bufio" "context" "fmt" "os/exec" @@ -26,8 +27,9 @@ type MultiNode struct { args chain.MultiNodeArgs nodeStatuses []NodeStatus - pids []int // Store the PIDs of the running processes - numNodes int // Number of nodes + pids []int // Store the PIDs of the running processes + numNodes int // Number of nodes + logs [][]string // Store logs for each node } type ToggleNodeMsg struct { @@ -39,6 +41,14 @@ type UpdateStatusMsg struct { status NodeStatus } +type UpdateLogsMsg struct{} + +func UpdateDeemon() tea.Cmd { + return func() tea.Msg { + return UpdateLogsMsg{} + } +} + // Initialize the model. func NewModel(ctx context.Context, chainname string, args chain.MultiNodeArgs) MultiNode { numNodes, err := strconv.Atoi(args.NumValidator) @@ -52,6 +62,7 @@ func NewModel(ctx context.Context, chainname string, args chain.MultiNodeArgs) M nodeStatuses: make([]NodeStatus, numNodes), // initial states of nodes pids: make([]int, numNodes), numNodes: numNodes, + logs: make([][]string, numNodes), // Initialize logs for each node } } @@ -68,7 +79,13 @@ func ToggleNode(nodeIdx int) tea.Cmd { } // Run or stop the node based on its status. -func RunNode(nodeIdx int, start bool, pid *int, args chain.MultiNodeArgs, appd string) tea.Cmd { +func RunNode(nodeIdx int, start bool, m MultiNode) tea.Cmd { + var ( + pid = &m.pids[nodeIdx] + args = m.args + appd = m.appd + ) + return func() tea.Msg { if start { nodeHome := filepath.Join(args.OutputDir, args.NodeDirPrefix+strconv.Itoa(nodeIdx)) @@ -80,7 +97,13 @@ func RunNode(nodeIdx int, start bool, pid *int, args chain.MultiNodeArgs, appd s Setpgid: true, // Ensure it runs in a new process group } - err := cmd.Start() // Start the node in the background + stdout, err := cmd.StdoutPipe() // Get stdout for logging + if err != nil { + fmt.Printf("Failed to start node %d: %v\n", nodeIdx+1, err) + return UpdateStatusMsg{nodeIdx: nodeIdx, status: Stopped} + } + + err = cmd.Start() // Start the node in the background if err != nil { fmt.Printf("Failed to start node %d: %v\n", nodeIdx+1, err) return UpdateStatusMsg{nodeIdx: nodeIdx, status: Stopped} @@ -88,8 +111,15 @@ func RunNode(nodeIdx int, start bool, pid *int, args chain.MultiNodeArgs, appd s *pid = cmd.Process.Pid // Store the PID go func() { - if err := cmd.Wait(); err != nil { - fmt.Printf("Node %d exited with error: %v\n", nodeIdx+1, err) + scanner := bufio.NewScanner(stdout) + for scanner.Scan() { + line := scanner.Text() + // Add log line to the respective node's log slice + m.logs[nodeIdx] = append(m.logs[nodeIdx], line) + // Keep only the last 5 lines + if len(m.logs[nodeIdx]) > 5 { + m.logs[nodeIdx] = m.logs[nodeIdx][len(m.logs[nodeIdx])-5:] + } } }() return UpdateStatusMsg{nodeIdx: nodeIdx, status: Running} @@ -111,7 +141,7 @@ func RunNode(nodeIdx int, start bool, pid *int, args chain.MultiNodeArgs, appd s func (m *MultiNode) StopAllNodes() { for i := 0; i < m.numNodes; i++ { if m.nodeStatuses[i] == Running { - RunNode(i, false, &m.pids[i], m.args, m.appd)() // Stop node + RunNode(i, false, *m)() // Stop node } } } @@ -135,13 +165,15 @@ func (m MultiNode) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case ToggleNodeMsg: if m.nodeStatuses[msg.nodeIdx] == Running { - return m, RunNode(msg.nodeIdx, false, &m.pids[msg.nodeIdx], m.args, m.appd) // Stop node + return m, RunNode(msg.nodeIdx, false, m) // Stop node } - return m, RunNode(msg.nodeIdx, true, &m.pids[msg.nodeIdx], m.args, m.appd) // Start node + return m, RunNode(msg.nodeIdx, true, m) // Start node case UpdateStatusMsg: m.nodeStatuses[msg.nodeIdx] = msg.status - return m, nil + return m, UpdateDeemon() + case UpdateLogsMsg: + return m, UpdateDeemon() } return m, nil @@ -149,23 +181,20 @@ func (m MultiNode) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // View renders the interface. func (m MultiNode) View() string { - statusText := func(status NodeStatus) string { - if status == Running { - return "[Running]" + output := "Node Control:\n" + for i := 0; i < m.numNodes; i++ { + status := "[Stopped]" + if m.nodeStatuses[i] == Running { + status = "[Running]" } - return "[Stopped]" - } - - infoNode := func(i int) string { - home := m.args.OutputDir - ipaddr := "tcp://127.0.0.1:" + strconv.Itoa(26657-3*i) - return fmt.Sprintf("INFO:| Home:%s | Node:%s ", home, ipaddr) + output += fmt.Sprintf("%d. Node %d %s --node tcp://127.0.0.1:%d:\n", i+1, i+1, status, 26657-3*i) + output += " [\n" + for _, line := range m.logs[i] { + output += " " + line + "\n" + } + output += " ]\n\n" } - output := "Press keys 1,2,3.. to start and stop node 1,2,3.. respectively \nNode Control:\n" - for i := 0; i < m.numNodes; i++ { - output += fmt.Sprintf("%d. Node %d %s -- %s\n", i+1, i+1, statusText(m.nodeStatuses[i]), infoNode(i)) - } output += "Press q to quit.\n" return output } From e7a3341c3f2012c5599c497b322bc3deae7799b3 Mon Sep 17 00:00:00 2001 From: Likes To Eat Fish Date: Mon, 7 Oct 2024 20:44:17 +0700 Subject: [PATCH 19/37] rename package model to bubblemodel --- ignite/cmd/{model => bubblemodel}/chain_debug.go | 0 ignite/cmd/{model => bubblemodel}/chain_debug_test.go | 4 ++-- ignite/cmd/{model => bubblemodel}/chain_serve.go | 0 ignite/cmd/{model => bubblemodel}/chain_serve_test.go | 4 ++-- ignite/cmd/{model => bubblemodel}/testdata/testdata.go | 0 ignite/cmd/{model => bubblemodel}/testnet_multi_node.go | 0 ignite/cmd/chain_debug.go | 2 +- ignite/cmd/chain_serve.go | 2 +- ignite/cmd/testnet_multi_node.go | 4 ++-- 9 files changed, 8 insertions(+), 8 deletions(-) rename ignite/cmd/{model => bubblemodel}/chain_debug.go (100%) rename ignite/cmd/{model => bubblemodel}/chain_debug_test.go (94%) rename ignite/cmd/{model => bubblemodel}/chain_serve.go (100%) rename ignite/cmd/{model => bubblemodel}/chain_serve_test.go (97%) rename ignite/cmd/{model => bubblemodel}/testdata/testdata.go (100%) rename ignite/cmd/{model => bubblemodel}/testnet_multi_node.go (100%) diff --git a/ignite/cmd/model/chain_debug.go b/ignite/cmd/bubblemodel/chain_debug.go similarity index 100% rename from ignite/cmd/model/chain_debug.go rename to ignite/cmd/bubblemodel/chain_debug.go diff --git a/ignite/cmd/model/chain_debug_test.go b/ignite/cmd/bubblemodel/chain_debug_test.go similarity index 94% rename from ignite/cmd/model/chain_debug_test.go rename to ignite/cmd/bubblemodel/chain_debug_test.go index eba2706a10..11dfce4174 100644 --- a/ignite/cmd/model/chain_debug_test.go +++ b/ignite/cmd/bubblemodel/chain_debug_test.go @@ -7,8 +7,8 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/stretchr/testify/require" - cmdmodel "github.com/ignite/cli/v29/ignite/cmd/model" - "github.com/ignite/cli/v29/ignite/cmd/model/testdata" + cmdmodel "github.com/ignite/cli/v29/ignite/cmd/bubblemodel" + "github.com/ignite/cli/v29/ignite/cmd/bubblemodel/testdata" "github.com/ignite/cli/v29/ignite/pkg/cliui/colors" "github.com/ignite/cli/v29/ignite/pkg/cliui/icons" cliuimodel "github.com/ignite/cli/v29/ignite/pkg/cliui/model" diff --git a/ignite/cmd/model/chain_serve.go b/ignite/cmd/bubblemodel/chain_serve.go similarity index 100% rename from ignite/cmd/model/chain_serve.go rename to ignite/cmd/bubblemodel/chain_serve.go diff --git a/ignite/cmd/model/chain_serve_test.go b/ignite/cmd/bubblemodel/chain_serve_test.go similarity index 97% rename from ignite/cmd/model/chain_serve_test.go rename to ignite/cmd/bubblemodel/chain_serve_test.go index 7a41481819..bd750c2317 100644 --- a/ignite/cmd/model/chain_serve_test.go +++ b/ignite/cmd/bubblemodel/chain_serve_test.go @@ -9,8 +9,8 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/stretchr/testify/require" - cmdmodel "github.com/ignite/cli/v29/ignite/cmd/model" - "github.com/ignite/cli/v29/ignite/cmd/model/testdata" + cmdmodel "github.com/ignite/cli/v29/ignite/cmd/bubblemodel" + "github.com/ignite/cli/v29/ignite/cmd/bubblemodel/testdata" "github.com/ignite/cli/v29/ignite/pkg/cliui/colors" "github.com/ignite/cli/v29/ignite/pkg/cliui/icons" cliuimodel "github.com/ignite/cli/v29/ignite/pkg/cliui/model" diff --git a/ignite/cmd/model/testdata/testdata.go b/ignite/cmd/bubblemodel/testdata/testdata.go similarity index 100% rename from ignite/cmd/model/testdata/testdata.go rename to ignite/cmd/bubblemodel/testdata/testdata.go diff --git a/ignite/cmd/model/testnet_multi_node.go b/ignite/cmd/bubblemodel/testnet_multi_node.go similarity index 100% rename from ignite/cmd/model/testnet_multi_node.go rename to ignite/cmd/bubblemodel/testnet_multi_node.go diff --git a/ignite/cmd/chain_debug.go b/ignite/cmd/chain_debug.go index fe84548cd3..773622a52c 100644 --- a/ignite/cmd/chain_debug.go +++ b/ignite/cmd/chain_debug.go @@ -7,7 +7,7 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/spf13/cobra" - cmdmodel "github.com/ignite/cli/v29/ignite/cmd/model" + cmdmodel "github.com/ignite/cli/v29/ignite/cmd/bubblemodel" chainconfig "github.com/ignite/cli/v29/ignite/config/chain" "github.com/ignite/cli/v29/ignite/pkg/chaincmd" "github.com/ignite/cli/v29/ignite/pkg/cliui" diff --git a/ignite/cmd/chain_serve.go b/ignite/cmd/chain_serve.go index 9b3c1c9207..c95735cc9e 100644 --- a/ignite/cmd/chain_serve.go +++ b/ignite/cmd/chain_serve.go @@ -6,7 +6,7 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/spf13/cobra" - cmdmodel "github.com/ignite/cli/v29/ignite/cmd/model" + cmdmodel "github.com/ignite/cli/v29/ignite/cmd/bubblemodel" "github.com/ignite/cli/v29/ignite/pkg/cliui" uilog "github.com/ignite/cli/v29/ignite/pkg/cliui/log" cliuimodel "github.com/ignite/cli/v29/ignite/pkg/cliui/model" diff --git a/ignite/cmd/testnet_multi_node.go b/ignite/cmd/testnet_multi_node.go index ff4c9e9518..7129cafdb0 100644 --- a/ignite/cmd/testnet_multi_node.go +++ b/ignite/cmd/testnet_multi_node.go @@ -11,8 +11,8 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" - cmdmodel "github.com/ignite/cli/v29/ignite/cmd/model" - "github.com/ignite/cli/v29/ignite/config/chain/v1" + cmdmodel "github.com/ignite/cli/v29/ignite/cmd/bubblemodel" + v1 "github.com/ignite/cli/v29/ignite/config/chain/v1" "github.com/ignite/cli/v29/ignite/pkg/cliui" "github.com/ignite/cli/v29/ignite/services/chain" ) From 3a2da5f72519288631842122e73b84e0082272c8 Mon Sep 17 00:00:00 2001 From: Duong NV | Decentrio Date: Wed, 9 Oct 2024 23:34:36 +0700 Subject: [PATCH 20/37] Update ignite/cmd/testnet_multi_node.go Co-authored-by: Julien Robert --- ignite/cmd/testnet_multi_node.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ignite/cmd/testnet_multi_node.go b/ignite/cmd/testnet_multi_node.go index 7129cafdb0..3887c677c9 100644 --- a/ignite/cmd/testnet_multi_node.go +++ b/ignite/cmd/testnet_multi_node.go @@ -110,7 +110,7 @@ func testnetMultiNode(cmd *cobra.Command, session *cliui.Session) error { } nodeDirPrefix, _ := cmd.Flags().GetString(flagNodeDirPrefix) - outputDir := filepath.Join(homeDir, ".ignite/local-chains/"+c.Name()+"d/testnet/") + outputDir := xfilepath.Join(config.DirPath, xfilepath.Path("local-chains").c.Name()+"d","testnet") args := chain.MultiNodeArgs{ OutputDir: outputDir, NumValidator: strconv.Itoa(numVal), From e86b9aae449f5b809e8ddcd41d03e2192256bb80 Mon Sep 17 00:00:00 2001 From: Likes To Eat Fish Date: Thu, 10 Oct 2024 00:11:10 +0700 Subject: [PATCH 21/37] nits --- ignite/cmd/testnet_multi_node.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ignite/cmd/testnet_multi_node.go b/ignite/cmd/testnet_multi_node.go index 3887c677c9..46405dce57 100644 --- a/ignite/cmd/testnet_multi_node.go +++ b/ignite/cmd/testnet_multi_node.go @@ -2,7 +2,6 @@ package ignitecmd import ( "os" - "path/filepath" "strconv" "time" @@ -12,8 +11,10 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" cmdmodel "github.com/ignite/cli/v29/ignite/cmd/bubblemodel" + igcfg "github.com/ignite/cli/v29/ignite/config" v1 "github.com/ignite/cli/v29/ignite/config/chain/v1" "github.com/ignite/cli/v29/ignite/pkg/cliui" + "github.com/ignite/cli/v29/ignite/pkg/xfilepath" "github.com/ignite/cli/v29/ignite/services/chain" ) @@ -104,13 +105,12 @@ func testnetMultiNode(cmd *cobra.Command, session *cliui.Session) error { if err != nil { return err } - homeDir, err := os.UserHomeDir() + nodeDirPrefix, _ := cmd.Flags().GetString(flagNodeDirPrefix) + + outputDir, err := xfilepath.Join(igcfg.DirPath, xfilepath.Path("local-chains/"+c.Name()+"d/"+"testnet/"))() if err != nil { return err } - nodeDirPrefix, _ := cmd.Flags().GetString(flagNodeDirPrefix) - - outputDir := xfilepath.Join(config.DirPath, xfilepath.Path("local-chains").c.Name()+"d","testnet") args := chain.MultiNodeArgs{ OutputDir: outputDir, NumValidator: strconv.Itoa(numVal), From 8f24f18d82a3b13cd5b20df4f03ca056784de7fc Mon Sep 17 00:00:00 2001 From: Duong NV | Decentrio Date: Sun, 13 Oct 2024 21:29:04 +0700 Subject: [PATCH 22/37] Update ignite/cmd/bubblemodel/testnet_multi_node.go Co-authored-by: Danilo Pantani --- ignite/cmd/bubblemodel/testnet_multi_node.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ignite/cmd/bubblemodel/testnet_multi_node.go b/ignite/cmd/bubblemodel/testnet_multi_node.go index d383fef8ba..64cdd6acae 100644 --- a/ignite/cmd/bubblemodel/testnet_multi_node.go +++ b/ignite/cmd/bubblemodel/testnet_multi_node.go @@ -49,7 +49,7 @@ func UpdateDeemon() tea.Cmd { } } -// Initialize the model. +// NewModel initializes the model. func NewModel(ctx context.Context, chainname string, args chain.MultiNodeArgs) MultiNode { numNodes, err := strconv.Atoi(args.NumValidator) if err != nil { From 176b342a5d957bb84927586651e5e50bcc0cab10 Mon Sep 17 00:00:00 2001 From: Duong NV | Decentrio Date: Sun, 13 Oct 2024 21:30:54 +0700 Subject: [PATCH 23/37] Update ignite/cmd/bubblemodel/testnet_multi_node.go Co-authored-by: Danilo Pantani --- ignite/cmd/bubblemodel/testnet_multi_node.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ignite/cmd/bubblemodel/testnet_multi_node.go b/ignite/cmd/bubblemodel/testnet_multi_node.go index 64cdd6acae..e308582705 100644 --- a/ignite/cmd/bubblemodel/testnet_multi_node.go +++ b/ignite/cmd/bubblemodel/testnet_multi_node.go @@ -66,7 +66,7 @@ func NewModel(ctx context.Context, chainname string, args chain.MultiNodeArgs) M } } -// Implement the Update function. +// Init implements the Update function. func (m MultiNode) Init() tea.Cmd { return nil } From 5c8593acc718c770b1e8b7a2a8497e35c90c10b4 Mon Sep 17 00:00:00 2001 From: Duong NV | Decentrio Date: Sun, 13 Oct 2024 21:31:24 +0700 Subject: [PATCH 24/37] Update ignite/cmd/bubblemodel/testnet_multi_node.go Co-authored-by: Danilo Pantani --- ignite/cmd/bubblemodel/testnet_multi_node.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ignite/cmd/bubblemodel/testnet_multi_node.go b/ignite/cmd/bubblemodel/testnet_multi_node.go index e308582705..20e91957e9 100644 --- a/ignite/cmd/bubblemodel/testnet_multi_node.go +++ b/ignite/cmd/bubblemodel/testnet_multi_node.go @@ -78,7 +78,7 @@ func ToggleNode(nodeIdx int) tea.Cmd { } } -// Run or stop the node based on its status. +// RunNode run or stop the node based on its status. func RunNode(nodeIdx int, start bool, m MultiNode) tea.Cmd { var ( pid = &m.pids[nodeIdx] From bf286dee0b1a134f27164e8db04d7029c9945309 Mon Sep 17 00:00:00 2001 From: Duong NV | Decentrio Date: Sun, 13 Oct 2024 21:36:42 +0700 Subject: [PATCH 25/37] Update ignite/cmd/testnet_multi_node.go Co-authored-by: Danilo Pantani --- ignite/cmd/testnet_multi_node.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ignite/cmd/testnet_multi_node.go b/ignite/cmd/testnet_multi_node.go index 46405dce57..bfce5ab7e5 100644 --- a/ignite/cmd/testnet_multi_node.go +++ b/ignite/cmd/testnet_multi_node.go @@ -6,9 +6,8 @@ import ( "time" tea "github.com/charmbracelet/bubbletea" - "github.com/spf13/cobra" - sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/spf13/cobra" cmdmodel "github.com/ignite/cli/v29/ignite/cmd/bubblemodel" igcfg "github.com/ignite/cli/v29/ignite/config" From 748fab829a58c8e2deae7493be22c2affe9c8631 Mon Sep 17 00:00:00 2001 From: Likes To Eat Fish Date: Sun, 13 Oct 2024 22:13:37 +0700 Subject: [PATCH 26/37] updates --- ignite/cmd/bubblemodel/testnet_multi_node.go | 12 ++++++++---- ignite/cmd/testnet_multi_node.go | 8 ++++++-- ignite/pkg/chaincmd/in-place-testnet.go | 11 +++++++++++ ignite/pkg/chaincmd/runner/chain.go | 1 + 4 files changed, 26 insertions(+), 6 deletions(-) diff --git a/ignite/cmd/bubblemodel/testnet_multi_node.go b/ignite/cmd/bubblemodel/testnet_multi_node.go index 20e91957e9..3f13506509 100644 --- a/ignite/cmd/bubblemodel/testnet_multi_node.go +++ b/ignite/cmd/bubblemodel/testnet_multi_node.go @@ -21,6 +21,8 @@ const ( Running ) +var _ tea.Model = MultiNode{} + type MultiNode struct { ctx context.Context appd string @@ -36,11 +38,13 @@ type ToggleNodeMsg struct { nodeIdx int } +// UpdateStatusMsg defines a message that updates the status of a node by index. type UpdateStatusMsg struct { nodeIdx int status NodeStatus } +// UpdateLogsMsg is for continuously updating the chain logs in the View. type UpdateLogsMsg struct{} func UpdateDeemon() tea.Cmd { @@ -50,10 +54,10 @@ func UpdateDeemon() tea.Cmd { } // NewModel initializes the model. -func NewModel(ctx context.Context, chainname string, args chain.MultiNodeArgs) MultiNode { +func NewModel(ctx context.Context, chainname string, args chain.MultiNodeArgs) (MultiNode, error) { numNodes, err := strconv.Atoi(args.NumValidator) if err != nil { - panic(err) + return MultiNode{}, err } return MultiNode{ ctx: ctx, @@ -63,10 +67,10 @@ func NewModel(ctx context.Context, chainname string, args chain.MultiNodeArgs) M pids: make([]int, numNodes), numNodes: numNodes, logs: make([][]string, numNodes), // Initialize logs for each node - } + }, nil } -// Init implements the Update function. +// Init implements the Init method of the tea.Model interface. func (m MultiNode) Init() tea.Cmd { return nil } diff --git a/ignite/cmd/testnet_multi_node.go b/ignite/cmd/testnet_multi_node.go index bfce5ab7e5..74711bdd63 100644 --- a/ignite/cmd/testnet_multi_node.go +++ b/ignite/cmd/testnet_multi_node.go @@ -133,8 +133,12 @@ func testnetMultiNode(cmd *cobra.Command, session *cliui.Session) error { time.Sleep(2 * time.Second) - m := cmdmodel.NewModel(cmd.Context(), c.Name(), args) - _, err = tea.NewProgram(m).Run() + model, err := cmdmodel.NewModel(cmd.Context(), c.Name(), args) + if err != nil { + return err + } + + _, err = tea.NewProgram(model).Run() return err } diff --git a/ignite/pkg/chaincmd/in-place-testnet.go b/ignite/pkg/chaincmd/in-place-testnet.go index cb8c7eb545..06191bcf77 100644 --- a/ignite/pkg/chaincmd/in-place-testnet.go +++ b/ignite/pkg/chaincmd/in-place-testnet.go @@ -46,8 +46,11 @@ func (c ChainCmd) TestnetInPlaceCommand(newChainID, newOperatorAddress string, o return c.daemonCommand(command) } +// Options for testnet multi node type MultiNodeOption func([]string) []string +// MultiNodeWithChainID returns a MultiNodeOption that appends the chainID option +// to the provided slice of strings func MultiNodeWithChainID(chainID string) MultiNodeOption { return func(s []string) []string { if len(chainID) > 0 { @@ -57,6 +60,8 @@ func MultiNodeWithChainID(chainID string) MultiNodeOption { } } +// MultiNodeWithDirOutput returns a MultiNodeOption that appends the output directory option +// to the provided slice of strings func MultiNodeWithDirOutput(dirOutput string) MultiNodeOption { return func(s []string) []string { if len(dirOutput) > 0 { @@ -66,6 +71,8 @@ func MultiNodeWithDirOutput(dirOutput string) MultiNodeOption { } } +// MultiNodeWithNumValidator returns a MultiNodeOption that appends the number of validators option +// to the provided slice of strings func MultiNodeWithNumValidator(numVal string) MultiNodeOption { return func(s []string) []string { if len(numVal) > 0 { @@ -75,6 +82,8 @@ func MultiNodeWithNumValidator(numVal string) MultiNodeOption { } } +// MultiNodeWithValidatorsStakeAmount returns a MultiNodeOption that appends the stake amounts option +// to the provided slice of strings func MultiNodeWithValidatorsStakeAmount(satkeAmounts string) MultiNodeOption { return func(s []string) []string { if len(satkeAmounts) > 0 { @@ -84,6 +93,8 @@ func MultiNodeWithValidatorsStakeAmount(satkeAmounts string) MultiNodeOption { } } +// MultiNodeDirPrefix returns a MultiNodeOption that appends the node directory prefix option +// to the provided slice of strings func MultiNodeDirPrefix(nodeDirPrefix string) MultiNodeOption { return func(s []string) []string { if len(nodeDirPrefix) > 0 { diff --git a/ignite/pkg/chaincmd/runner/chain.go b/ignite/pkg/chaincmd/runner/chain.go index ea39c315ac..4130d01312 100644 --- a/ignite/pkg/chaincmd/runner/chain.go +++ b/ignite/pkg/chaincmd/runner/chain.go @@ -57,6 +57,7 @@ func (r Runner) InPlace(ctx context.Context, newChainID, newOperatorAddress stri ) } +// Initialize config directories & files for a multi-validator testnet locally func (r Runner) MultiNode(ctx context.Context, options ...chaincmd.MultiNodeOption) error { runOptions := runOptions{ stdout: os.Stdout, From c89e7f4e61e80280b73d3ea0fad66d3fea679569 Mon Sep 17 00:00:00 2001 From: Likes To Eat Fish Date: Sun, 13 Oct 2024 23:11:22 +0700 Subject: [PATCH 27/37] use lipgloss for View --- ignite/cmd/bubblemodel/testnet_multi_node.go | 30 +++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/ignite/cmd/bubblemodel/testnet_multi_node.go b/ignite/cmd/bubblemodel/testnet_multi_node.go index 3f13506509..0530aa1cbd 100644 --- a/ignite/cmd/bubblemodel/testnet_multi_node.go +++ b/ignite/cmd/bubblemodel/testnet_multi_node.go @@ -10,6 +10,7 @@ import ( "syscall" tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" "github.com/ignite/cli/v29/ignite/services/chain" ) @@ -185,20 +186,35 @@ func (m MultiNode) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // View renders the interface. func (m MultiNode) View() string { - output := "Node Control:\n" + // Define styles for the state + runningStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("2")) // green + stoppedStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("1")) // red + tcpStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("3")) // yellow + grayStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("8")) // gray + purpleStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("5")) // purple + + output := purpleStyle.Render("Node Control:") for i := 0; i < m.numNodes; i++ { - status := "[Stopped]" + status := stoppedStyle.Render("[Stopped]") if m.nodeStatuses[i] == Running { - status = "[Running]" + status = runningStyle.Render("[Running]") } - output += fmt.Sprintf("%d. Node %d %s --node tcp://127.0.0.1:%d:\n", i+1, i+1, status, 26657-3*i) + + tcpAddress := tcpStyle.Render(fmt.Sprintf("tcp://127.0.0.1:%d", 26657-3*i)) + nodeGray := grayStyle.Render("--node") + nodeNumber := purpleStyle.Render(fmt.Sprintf("%d.", i+1)) + + output += fmt.Sprintf("\n%s Node %d %s %s %s:\n", nodeNumber, i+1, status, nodeGray, tcpAddress) output += " [\n" - for _, line := range m.logs[i] { - output += " " + line + "\n" + if m.logs != nil { + for _, line := range m.logs[i] { + output += " " + line + "\n" + } } + output += " ]\n\n" } - output += "Press q to quit.\n" + output += grayStyle.Render("Press q to quit.\n") return output } From b26b894194f2a3f7f66366664cfc78636d18e922 Mon Sep 17 00:00:00 2001 From: Likes To Eat Fish Date: Mon, 14 Oct 2024 10:23:08 +0700 Subject: [PATCH 28/37] status bar --- ignite/cmd/bubblemodel/testnet_multi_node.go | 22 +++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/ignite/cmd/bubblemodel/testnet_multi_node.go b/ignite/cmd/bubblemodel/testnet_multi_node.go index 0530aa1cbd..ce1fdcdb19 100644 --- a/ignite/cmd/bubblemodel/testnet_multi_node.go +++ b/ignite/cmd/bubblemodel/testnet_multi_node.go @@ -187,13 +187,19 @@ func (m MultiNode) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // View renders the interface. func (m MultiNode) View() string { // Define styles for the state - runningStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("2")) // green - stoppedStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("1")) // red - tcpStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("3")) // yellow - grayStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("8")) // gray - purpleStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("5")) // purple - - output := purpleStyle.Render("Node Control:") + runningStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("2")) // green + stoppedStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("1")) // red + tcpStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("3")) // yellow + grayStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("8")) // gray + purpleStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("5")) // purple + statusBarStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("5")).Background(lipgloss.Color("0")) // Status bar style + + // Add status bar at the top with action information + statusBar := statusBarStyle.Render("Press q to quit | Press 1-4 to start/stop corresponding node") + output := statusBar + "\n\n" + + // Add node control section + output += purpleStyle.Render("Node Control:") for i := 0; i < m.numNodes; i++ { status := stoppedStyle.Render("[Stopped]") if m.nodeStatuses[i] == Running { @@ -215,6 +221,6 @@ func (m MultiNode) View() string { output += " ]\n\n" } - output += grayStyle.Render("Press q to quit.\n") + output += grayStyle.Render("\nPress q to quit.\n") return output } From 080f811ad5363c129a4b9cc69d68a4b8eb0d4833 Mon Sep 17 00:00:00 2001 From: Likes To Eat Fish Date: Mon, 14 Oct 2024 10:39:52 +0700 Subject: [PATCH 29/37] nits --- ignite/cmd/bubblemodel/testnet_multi_node.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/ignite/cmd/bubblemodel/testnet_multi_node.go b/ignite/cmd/bubblemodel/testnet_multi_node.go index ce1fdcdb19..c3f46d3063 100644 --- a/ignite/cmd/bubblemodel/testnet_multi_node.go +++ b/ignite/cmd/bubblemodel/testnet_multi_node.go @@ -187,15 +187,15 @@ func (m MultiNode) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // View renders the interface. func (m MultiNode) View() string { // Define styles for the state - runningStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("2")) // green - stoppedStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("1")) // red - tcpStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("3")) // yellow - grayStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("8")) // gray - purpleStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("5")) // purple - statusBarStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("5")).Background(lipgloss.Color("0")) // Status bar style - - // Add status bar at the top with action information - statusBar := statusBarStyle.Render("Press q to quit | Press 1-4 to start/stop corresponding node") + runningStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("2")) // green + stoppedStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("1")) // red + tcpStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("3")) // yellow + grayStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("8")) // gray + purpleStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("5")) // purple + statusBarStyle := lipgloss.NewStyle().Background(lipgloss.Color("0")) // Status bar style + blueStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("45")).Background(lipgloss.Color("0")) //blue + + statusBar := blueStyle.Render("Press q to quit | Press 1-4 to ") + statusBarStyle.Render(runningStyle.Render("start")) + blueStyle.Render("/") + statusBarStyle.Render(stoppedStyle.Render("stop")) + blueStyle.Render(" corresponding node") output := statusBar + "\n\n" // Add node control section From b25b9d485d801ffe46b7f4723e9162630b1b046b Mon Sep 17 00:00:00 2001 From: Duong NV | Decentrio Date: Mon, 14 Oct 2024 22:12:36 +0700 Subject: [PATCH 30/37] Update ignite/cmd/bubblemodel/testnet_multi_node.go Co-authored-by: Danilo Pantani --- ignite/cmd/bubblemodel/testnet_multi_node.go | 1 - 1 file changed, 1 deletion(-) diff --git a/ignite/cmd/bubblemodel/testnet_multi_node.go b/ignite/cmd/bubblemodel/testnet_multi_node.go index c3f46d3063..6cdea37931 100644 --- a/ignite/cmd/bubblemodel/testnet_multi_node.go +++ b/ignite/cmd/bubblemodel/testnet_multi_node.go @@ -25,7 +25,6 @@ const ( var _ tea.Model = MultiNode{} type MultiNode struct { - ctx context.Context appd string args chain.MultiNodeArgs From 697e05c29bf4ef18be22ee9bce78557da5a67d98 Mon Sep 17 00:00:00 2001 From: Duong NV | Decentrio Date: Mon, 14 Oct 2024 22:13:07 +0700 Subject: [PATCH 31/37] Update ignite/cmd/bubblemodel/testnet_multi_node.go Co-authored-by: Danilo Pantani --- ignite/cmd/bubblemodel/testnet_multi_node.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ignite/cmd/bubblemodel/testnet_multi_node.go b/ignite/cmd/bubblemodel/testnet_multi_node.go index 6cdea37931..2edfd6ff27 100644 --- a/ignite/cmd/bubblemodel/testnet_multi_node.go +++ b/ignite/cmd/bubblemodel/testnet_multi_node.go @@ -141,7 +141,7 @@ func RunNode(nodeIdx int, start bool, m MultiNode) tea.Cmd { } } -// Stop all nodes. +// StopAllNodes stops all nodes. func (m *MultiNode) StopAllNodes() { for i := 0; i < m.numNodes; i++ { if m.nodeStatuses[i] == Running { From 28d9df7f9a3d65586d0ea5ad23dbf5b60d853bc5 Mon Sep 17 00:00:00 2001 From: Likes To Eat Fish Date: Mon, 14 Oct 2024 22:15:43 +0700 Subject: [PATCH 32/37] remove ctx --- ignite/cmd/bubblemodel/testnet_multi_node.go | 4 +--- ignite/cmd/testnet_multi_node.go | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/ignite/cmd/bubblemodel/testnet_multi_node.go b/ignite/cmd/bubblemodel/testnet_multi_node.go index 2edfd6ff27..faf1d22f96 100644 --- a/ignite/cmd/bubblemodel/testnet_multi_node.go +++ b/ignite/cmd/bubblemodel/testnet_multi_node.go @@ -2,7 +2,6 @@ package cmdmodel import ( "bufio" - "context" "fmt" "os/exec" "path/filepath" @@ -54,13 +53,12 @@ func UpdateDeemon() tea.Cmd { } // NewModel initializes the model. -func NewModel(ctx context.Context, chainname string, args chain.MultiNodeArgs) (MultiNode, error) { +func NewModel(chainname string, args chain.MultiNodeArgs) (MultiNode, error) { numNodes, err := strconv.Atoi(args.NumValidator) if err != nil { return MultiNode{}, err } return MultiNode{ - ctx: ctx, appd: chainname + "d", args: args, nodeStatuses: make([]NodeStatus, numNodes), // initial states of nodes diff --git a/ignite/cmd/testnet_multi_node.go b/ignite/cmd/testnet_multi_node.go index 74711bdd63..ab87408837 100644 --- a/ignite/cmd/testnet_multi_node.go +++ b/ignite/cmd/testnet_multi_node.go @@ -133,7 +133,7 @@ func testnetMultiNode(cmd *cobra.Command, session *cliui.Session) error { time.Sleep(2 * time.Second) - model, err := cmdmodel.NewModel(cmd.Context(), c.Name(), args) + model, err := cmdmodel.NewModel(c.Name(), args) if err != nil { return err } From 6a9a4f4018e26632d730ec1341ab05d0396fbf68 Mon Sep 17 00:00:00 2001 From: Likes To Eat Fish Date: Mon, 14 Oct 2024 22:22:51 +0700 Subject: [PATCH 33/37] add comment --- ignite/cmd/bubblemodel/testnet_multi_node.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/ignite/cmd/bubblemodel/testnet_multi_node.go b/ignite/cmd/bubblemodel/testnet_multi_node.go index faf1d22f96..c7316edd73 100644 --- a/ignite/cmd/bubblemodel/testnet_multi_node.go +++ b/ignite/cmd/bubblemodel/testnet_multi_node.go @@ -14,15 +14,21 @@ import ( "github.com/ignite/cli/v29/ignite/services/chain" ) +// NodeStatus is an integer data type that represents the status of a node. type NodeStatus int const ( + // Stopped indicates that the node is currently stopped. Stopped NodeStatus = iota + + // Running indicates that the node is currently running. Running ) +// Make sure MultiNode implements tea.Model interface. var _ tea.Model = MultiNode{} +// MultiNode represents a set of nodes, managing state and information related to them. type MultiNode struct { appd string args chain.MultiNodeArgs @@ -33,6 +39,8 @@ type MultiNode struct { logs [][]string // Store logs for each node } +// ToggleNodeMsg is a structure used to pass messages +// to enable or disable a node based on the node index. type ToggleNodeMsg struct { nodeIdx int } @@ -46,6 +54,8 @@ type UpdateStatusMsg struct { // UpdateLogsMsg is for continuously updating the chain logs in the View. type UpdateLogsMsg struct{} +// UpdateDeemon returns a command that sends an UpdateLogsMsg. +// This command is intended to continuously refresh the logs displayed in the user interface. func UpdateDeemon() tea.Cmd { return func() tea.Msg { return UpdateLogsMsg{} From 67d3e88ce34c1c6f1b2bc993d7d8112bcdb7f4ae Mon Sep 17 00:00:00 2001 From: Likes To Eat Fish Date: Wed, 16 Oct 2024 23:46:04 +0700 Subject: [PATCH 34/37] use ports in ignite/pkg/availableport/availableport.go --- ignite/cmd/bubblemodel/testnet_multi_node.go | 2 +- ignite/cmd/testnet_multi_node.go | 8 ++++ ignite/pkg/chaincmd/chaincmd.go | 1 + ignite/pkg/chaincmd/in-place-testnet.go | 9 ++++ ignite/services/chain/runtime.go | 1 + ignite/services/chain/testnet.go | 13 ++++++ .../cmd/testnet_multi_node.go.plush | 42 +++++++++++++++---- 7 files changed, 66 insertions(+), 10 deletions(-) diff --git a/ignite/cmd/bubblemodel/testnet_multi_node.go b/ignite/cmd/bubblemodel/testnet_multi_node.go index c7316edd73..ecf2dbc0c4 100644 --- a/ignite/cmd/bubblemodel/testnet_multi_node.go +++ b/ignite/cmd/bubblemodel/testnet_multi_node.go @@ -213,7 +213,7 @@ func (m MultiNode) View() string { status = runningStyle.Render("[Running]") } - tcpAddress := tcpStyle.Render(fmt.Sprintf("tcp://127.0.0.1:%d", 26657-3*i)) + tcpAddress := tcpStyle.Render(fmt.Sprintf("tcp://127.0.0.1:%d", m.args.ListPorts[i])) nodeGray := grayStyle.Render("--node") nodeNumber := purpleStyle.Render(fmt.Sprintf("%d.", i+1)) diff --git a/ignite/cmd/testnet_multi_node.go b/ignite/cmd/testnet_multi_node.go index ab87408837..fbfe0aabef 100644 --- a/ignite/cmd/testnet_multi_node.go +++ b/ignite/cmd/testnet_multi_node.go @@ -12,6 +12,7 @@ import ( cmdmodel "github.com/ignite/cli/v29/ignite/cmd/bubblemodel" igcfg "github.com/ignite/cli/v29/ignite/config" v1 "github.com/ignite/cli/v29/ignite/config/chain/v1" + "github.com/ignite/cli/v29/ignite/pkg/availableport" "github.com/ignite/cli/v29/ignite/pkg/cliui" "github.com/ignite/cli/v29/ignite/pkg/xfilepath" "github.com/ignite/cli/v29/ignite/services/chain" @@ -110,11 +111,18 @@ func testnetMultiNode(cmd *cobra.Command, session *cliui.Session) error { if err != nil { return err } + + ports, err := availableport.Find(uint(numVal)) + if err != nil { + return err + } + args := chain.MultiNodeArgs{ OutputDir: outputDir, NumValidator: strconv.Itoa(numVal), ValidatorsStakeAmount: amountDetails, NodeDirPrefix: nodeDirPrefix, + ListPorts: ports, } resetOnce, _ := cmd.Flags().GetBool(flagResetOnce) diff --git a/ignite/pkg/chaincmd/chaincmd.go b/ignite/pkg/chaincmd/chaincmd.go index c8785f63d6..be8c55bc25 100644 --- a/ignite/pkg/chaincmd/chaincmd.go +++ b/ignite/pkg/chaincmd/chaincmd.go @@ -64,6 +64,7 @@ const ( optionOutPutDir = "--output-dir" optionNumValidator = "--v" optionNodeDirPrefix = "--node-dir-prefix" + optionPorts = "--list-ports" constTendermint = "tendermint" constJSON = "json" diff --git a/ignite/pkg/chaincmd/in-place-testnet.go b/ignite/pkg/chaincmd/in-place-testnet.go index 06191bcf77..10313c11fe 100644 --- a/ignite/pkg/chaincmd/in-place-testnet.go +++ b/ignite/pkg/chaincmd/in-place-testnet.go @@ -104,6 +104,15 @@ func MultiNodeDirPrefix(nodeDirPrefix string) MultiNodeOption { } } +func MultiNodePorts(ports string) MultiNodeOption { + return func(s []string) []string { + if len(ports) > 0 { + return append(s, optionPorts, ports) + } + return s + } +} + // TestnetMultiNodeCommand return command to start testnet multinode. func (c ChainCmd) TestnetMultiNodeCommand(options ...MultiNodeOption) step.Option { command := []string{ diff --git a/ignite/services/chain/runtime.go b/ignite/services/chain/runtime.go index 63bf1e2db7..15af380016 100644 --- a/ignite/services/chain/runtime.go +++ b/ignite/services/chain/runtime.go @@ -55,6 +55,7 @@ func (c Chain) MultiNode(ctx context.Context, runner chaincmdrunner.Runner, args chaincmd.MultiNodeWithNumValidator(args.NumValidator), chaincmd.MultiNodeWithValidatorsStakeAmount(args.ValidatorsStakeAmount), chaincmd.MultiNodeDirPrefix(args.NodeDirPrefix), + chaincmd.MultiNodePorts(args.ConvertPorts()), ) return err } diff --git a/ignite/services/chain/testnet.go b/ignite/services/chain/testnet.go index f1ac388819..50efed36ec 100644 --- a/ignite/services/chain/testnet.go +++ b/ignite/services/chain/testnet.go @@ -2,7 +2,9 @@ package chain import ( "context" + "fmt" "os" + "strings" chainconfig "github.com/ignite/cli/v29/ignite/config/chain" ) @@ -41,6 +43,17 @@ type MultiNodeArgs struct { NumValidator string ValidatorsStakeAmount string NodeDirPrefix string + ListPorts []uint +} + +func (m MultiNodeArgs) ConvertPorts() string { + var result []string + + for _, port := range m.ListPorts { + result = append(result, fmt.Sprintf("%d", port)) + } + + return strings.Join(result, ",") } // If the app state still exists, TestnetMultiNode will reuse it. diff --git a/ignite/templates/app/files/cmd/{{binaryNamePrefix}}d/cmd/testnet_multi_node.go.plush b/ignite/templates/app/files/cmd/{{binaryNamePrefix}}d/cmd/testnet_multi_node.go.plush index 4691704a7d..8a02caab08 100644 --- a/ignite/templates/app/files/cmd/{{binaryNamePrefix}}d/cmd/testnet_multi_node.go.plush +++ b/ignite/templates/app/files/cmd/{{binaryNamePrefix}}d/cmd/testnet_multi_node.go.plush @@ -41,6 +41,7 @@ import ( var ( flagNodeDirPrefix = "node-dir-prefix" + flagPorts = "list-ports" flagNumValidators = "v" flagOutputDir = "output-dir" flagValidatorsStakeAmount = "validators-stake-amount" @@ -59,6 +60,7 @@ type initArgs struct { outputDir string startingIPAddress string validatorsStakesAmount map[int]sdk.Coin + ports map[int]string } // NewTestnetMultiNodeCmd returns a cmd to initialize all files for tendermint testnet and application @@ -75,7 +77,7 @@ or a similar setup where each node has a manually configurable IP address. Note, strict routability for addresses is turned off in the config file. Example: - <%= AppName %>d multi-node --v 4 --output-dir ./.testnets --validators-stake-amount 1000000,200000,300000,400000 + <%= AppName %>d multi-node --v 4 --output-dir ./.testnets --validators-stake-amount 1000000,200000,300000,400000 --list-ports 47222,50434,52851,44210 `, RunE: func(cmd *cobra.Command, _ []string) error { clientCtx, err := client.GetClientQueryContext(cmd) @@ -96,6 +98,7 @@ Example: args.numValidators, _ = cmd.Flags().GetInt(flagNumValidators) args.algo, _ = cmd.Flags().GetString(flags.FlagKeyType) + args.ports = map[int]string{} args.validatorsStakesAmount = make(map[int]sdk.Coin) top := 0 // If the flag string is invalid, the amount will default to 100000000. @@ -110,12 +113,27 @@ Example: } } + top = 0 + if s, err := cmd.Flags().GetString(flagPorts); err == nil { + if s == "" { + for i := 0; i < args.numValidators; i++ { + args.ports[top] = strconv.Itoa(26657 - 3*i) + top += 1 + } + } else { + for _, port := range strings.Split(s, ",") { + args.ports[top] = port + top += 1 + } + } + } return initTestnetFiles(clientCtx, cmd, config, mbm, genBalIterator, args) }, } addTestnetFlagsToCmd(cmd) + cmd.Flags().String(flagPorts, "", "Ports of nodes (default 26657,26654,26651,26648.. )") cmd.Flags().String(flagNodeDirPrefix, "validator", "Prefix the directory name for each node with (node results in node0, node1, ...)") cmd.Flags().String(flagValidatorsStakeAmount, "100000000,100000000,100000000,100000000", "Amount of stake for each validator") cmd.Flags().String(flagStartingIPAddress, "localhost", "Starting IP address (192.168.0.1 results in persistent peers list ID0@192.168.0.1:46656, ID1@192.168.0.2:46656, ...)") @@ -180,7 +198,7 @@ func initTestnetFiles( nodeConfig.SetRoot(nodeDir) nodeConfig.Moniker = nodeDirName - nodeConfig.RPC.ListenAddress = "tcp://0.0.0.0:" + strconv.Itoa(26657-3*i) + nodeConfig.RPC.ListenAddress = "tcp://0.0.0.0:" + args.ports[i] var err error if err := os.MkdirAll(filepath.Join(nodeDir, "config"), nodeDirPerm); err != nil { @@ -312,10 +330,10 @@ func initTestnetFiles( } } err := collectGenFiles( - clientCtx, nodeConfig, args.chainID, nodeIDs, valPubKeys, args.numValidators, - args.outputDir, args.nodeDirPrefix, genBalIterator, + clientCtx, nodeConfig, nodeIDs, valPubKeys, + genBalIterator, clientCtx.TxConfig.SigningContext().ValidatorAddressCodec(), - persistentPeers, + persistentPeers, args, ) if err != nil { return err @@ -387,11 +405,17 @@ func initGenFiles( } func collectGenFiles( - clientCtx client.Context, nodeConfig *cmtconfig.Config, chainID string, - nodeIDs []string, valPubKeys []cryptotypes.PubKey, numValidators int, - outputDir, nodeDirPrefix string, genBalIterator banktypes.GenesisBalancesIterator, + clientCtx client.Context, nodeConfig *cmtconfig.Config, + nodeIDs []string, valPubKeys []cryptotypes.PubKey, + genBalIterator banktypes.GenesisBalancesIterator, valAddrCodec runtime.ValidatorAddressCodec, persistentPeers string, + args initArgs, ) error { + chainID := args.chainID + numValidators := args.numValidators + outputDir := args.outputDir + nodeDirPrefix := args.nodeDirPrefix + var appState json.RawMessage genTime := tmtime.Now() @@ -420,7 +444,7 @@ func collectGenFiles( nodeConfig.P2P.PersistentPeers = persistentPeers nodeConfig.P2P.AllowDuplicateIP = true nodeConfig.P2P.ListenAddress = "tcp://0.0.0.0:" + strconv.Itoa(26656-3*i) - nodeConfig.RPC.ListenAddress = "tcp://127.0.0.1:" + strconv.Itoa(26657-3*i) + nodeConfig.RPC.ListenAddress = "tcp://127.0.0.1:" + args.ports[i] nodeConfig.BaseConfig.ProxyApp = "tcp://127.0.0.1:" + strconv.Itoa(26658-3*i) nodeConfig.Instrumentation.PrometheusListenAddr = ":" + strconv.Itoa(26660+i) nodeConfig.Instrumentation.Prometheus = true From 2b0fc013a6c19d16b9851834030822758f5a29d7 Mon Sep 17 00:00:00 2001 From: Likes To Eat Fish Date: Thu, 17 Oct 2024 00:22:14 +0700 Subject: [PATCH 35/37] update errgroup --- ignite/cmd/bubblemodel/testnet_multi_node.go | 58 ++++++++++++++++---- ignite/cmd/testnet_multi_node.go | 2 +- 2 files changed, 48 insertions(+), 12 deletions(-) diff --git a/ignite/cmd/bubblemodel/testnet_multi_node.go b/ignite/cmd/bubblemodel/testnet_multi_node.go index ecf2dbc0c4..0ae5eb14c4 100644 --- a/ignite/cmd/bubblemodel/testnet_multi_node.go +++ b/ignite/cmd/bubblemodel/testnet_multi_node.go @@ -2,6 +2,7 @@ package cmdmodel import ( "bufio" + "context" "fmt" "os/exec" "path/filepath" @@ -10,6 +11,7 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" + "golang.org/x/sync/errgroup" "github.com/ignite/cli/v29/ignite/services/chain" ) @@ -30,6 +32,7 @@ var _ tea.Model = MultiNode{} // MultiNode represents a set of nodes, managing state and information related to them. type MultiNode struct { + ctx context.Context appd string args chain.MultiNodeArgs @@ -63,12 +66,13 @@ func UpdateDeemon() tea.Cmd { } // NewModel initializes the model. -func NewModel(chainname string, args chain.MultiNodeArgs) (MultiNode, error) { +func NewModel(ctx context.Context, chainname string, args chain.MultiNodeArgs) (MultiNode, error) { numNodes, err := strconv.Atoi(args.NumValidator) if err != nil { return MultiNode{}, err } return MultiNode{ + ctx: ctx, appd: chainname + "d", args: args, nodeStatuses: make([]NodeStatus, numNodes), // initial states of nodes @@ -90,7 +94,7 @@ func ToggleNode(nodeIdx int) tea.Cmd { } } -// RunNode run or stop the node based on its status. +// RunNode runs or stops the node based on its status. func RunNode(nodeIdx int, start bool, m MultiNode) tea.Cmd { var ( pid = &m.pids[nodeIdx] @@ -101,7 +105,7 @@ func RunNode(nodeIdx int, start bool, m MultiNode) tea.Cmd { return func() tea.Msg { if start { nodeHome := filepath.Join(args.OutputDir, args.NodeDirPrefix+strconv.Itoa(nodeIdx)) - // Create the command to run in background as a daemon + // Create the command to run in the background as a daemon cmd := exec.Command(appd, "start", "--home", nodeHome) // Start the process as a daemon @@ -122,18 +126,50 @@ func RunNode(nodeIdx int, start bool, m MultiNode) tea.Cmd { } *pid = cmd.Process.Pid // Store the PID - go func() { + + // Create an errgroup with context + g, gCtx := errgroup.WithContext(m.ctx) + g.Go(func() error { scanner := bufio.NewScanner(stdout) for scanner.Scan() { - line := scanner.Text() - // Add log line to the respective node's log slice - m.logs[nodeIdx] = append(m.logs[nodeIdx], line) - // Keep only the last 5 lines - if len(m.logs[nodeIdx]) > 5 { - m.logs[nodeIdx] = m.logs[nodeIdx][len(m.logs[nodeIdx])-5:] + select { + case <-gCtx.Done(): + // Handle context cancellation + return gCtx.Err() + default: + + line := scanner.Text() + // Add log line to the respective node's log slice + m.logs[nodeIdx] = append(m.logs[nodeIdx], line) + // Keep only the last 5 lines + if len(m.logs[nodeIdx]) > 5 { + m.logs[nodeIdx] = m.logs[nodeIdx][len(m.logs[nodeIdx])-5:] + } + } + } + if err := scanner.Err(); err != nil { + return err + } + return nil + }) + + // Goroutine to handle stopping the node if context is canceled + g.Go(func() error { + <-gCtx.Done() // Wait for context to be canceled + + // Stop the daemon process if context is canceled + if *pid != 0 { + err := syscall.Kill(-*pid, syscall.SIGTERM) // Stop the daemon process + if err != nil { + fmt.Printf("Failed to stop node %d: %v\n", nodeIdx+1, err) + } else { + *pid = 0 // Reset PID after stopping } } - }() + + return gCtx.Err() + }) + return UpdateStatusMsg{nodeIdx: nodeIdx, status: Running} } // Use kill to stop the node process by PID diff --git a/ignite/cmd/testnet_multi_node.go b/ignite/cmd/testnet_multi_node.go index fbfe0aabef..0a3db23689 100644 --- a/ignite/cmd/testnet_multi_node.go +++ b/ignite/cmd/testnet_multi_node.go @@ -141,7 +141,7 @@ func testnetMultiNode(cmd *cobra.Command, session *cliui.Session) error { time.Sleep(2 * time.Second) - model, err := cmdmodel.NewModel(c.Name(), args) + model, err := cmdmodel.NewModel(cmd.Context(), c.Name(), args) if err != nil { return err } From e9e8dd2f7bdeda817bd8b0764a4b838a49010596 Mon Sep 17 00:00:00 2001 From: Duong NV | Decentrio Date: Sun, 20 Oct 2024 13:12:12 +0700 Subject: [PATCH 36/37] Update changelog.md Co-authored-by: Danilo Pantani --- changelog.md | 1 - 1 file changed, 1 deletion(-) diff --git a/changelog.md b/changelog.md index cfb3c2a2e9..708e79f889 100644 --- a/changelog.md +++ b/changelog.md @@ -18,7 +18,6 @@ - [#4131](https://github.com/ignite/cli/pull/4131) Support `bytes` as data type in the `scaffold` commands - [#4300](https://github.com/ignite/cli/pull/4300) Only panics the module in the most top function level - [#4327](https://github.com/ignite/cli/pull/4327) Use the TxConfig from simState instead create a new one -- [#4326](https://github.com/ignite/cli/pull/4326) fAdd `buf.build` version to `ignite version` command - [#4377](https://github.com/ignite/cli/pull/4377) Add multi node (validator) testnet. - [#4326](https://github.com/ignite/cli/pull/4326) Add `buf.build` version to `ignite version` command - [#4362](https://github.com/ignite/cli/pull/4362) Scaffold `Makefile` From 7e2453867776bd594c565b0680727b26b9b4b535 Mon Sep 17 00:00:00 2001 From: Likes To Eat Fish Date: Sun, 20 Oct 2024 14:11:20 +0700 Subject: [PATCH 37/37] updates with v0.52 --- .../cmd/commands.go.plush | 4 +- .../cmd/testnet_multi_node.go.plush | 51 ++++++++++--------- 2 files changed, 29 insertions(+), 26 deletions(-) diff --git a/ignite/templates/app/files/cmd/{{binaryNamePrefix}}d/cmd/commands.go.plush b/ignite/templates/app/files/cmd/{{binaryNamePrefix}}d/cmd/commands.go.plush index 108a444a72..5361937f59 100644 --- a/ignite/templates/app/files/cmd/{{binaryNamePrefix}}d/cmd/commands.go.plush +++ b/ignite/templates/app/files/cmd/{{binaryNamePrefix}}d/cmd/commands.go.plush @@ -23,8 +23,6 @@ import ( "github.com/cosmos/cosmos-sdk/types/module" authcmd "github.com/cosmos/cosmos-sdk/x/auth/client/cli" "github.com/cosmos/cosmos-sdk/x/genutil" - banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" - "github.com/cosmos/cosmos-sdk/x/crisis" genutilcli "github.com/cosmos/cosmos-sdk/x/genutil/client/cli" genutiltypes "github.com/cosmos/cosmos-sdk/x/genutil/types" @@ -38,7 +36,7 @@ func initRootCmd( rootCmd.AddCommand( genutilcli.InitCmd(moduleManager), NewInPlaceTestnetCmd(), - NewTestnetMultiNodeCmd(basicManager, banktypes.GenesisBalancesIterator{}), + NewTestnetMultiNodeCmd(moduleManager), debug.Cmd(), confixcmd.ConfigCommand(), pruning.Cmd(newApp), diff --git a/ignite/templates/app/files/cmd/{{binaryNamePrefix}}d/cmd/testnet_multi_node.go.plush b/ignite/templates/app/files/cmd/{{binaryNamePrefix}}d/cmd/testnet_multi_node.go.plush index 8a02caab08..0f6532b750 100644 --- a/ignite/templates/app/files/cmd/{{binaryNamePrefix}}d/cmd/testnet_multi_node.go.plush +++ b/ignite/templates/app/files/cmd/{{binaryNamePrefix}}d/cmd/testnet_multi_node.go.plush @@ -19,11 +19,14 @@ import ( "github.com/spf13/pflag" "cosmossdk.io/math" + banktypes "cosmossdk.io/x/bank/types" + stakingtypes "cosmossdk.io/x/staking/types" "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/flags" "github.com/cosmos/cosmos-sdk/client/tx" "github.com/cosmos/cosmos-sdk/crypto/hd" "github.com/cosmos/cosmos-sdk/crypto/keyring" + "github.com/cosmos/cosmos-sdk/crypto/keys/ed25519" cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" "github.com/cosmos/cosmos-sdk/server" srvconfig "github.com/cosmos/cosmos-sdk/server/config" @@ -31,12 +34,8 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" - banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" "github.com/cosmos/cosmos-sdk/x/genutil" genutiltypes "github.com/cosmos/cosmos-sdk/x/genutil/types" - stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" - - runtime "github.com/cosmos/cosmos-sdk/runtime" ) var ( @@ -64,7 +63,7 @@ type initArgs struct { } // NewTestnetMultiNodeCmd returns a cmd to initialize all files for tendermint testnet and application -func NewTestnetMultiNodeCmd(mbm module.BasicManager, genBalIterator banktypes.GenesisBalancesIterator) *cobra.Command { +func NewTestnetMultiNodeCmd(mbm *module.Manager) *cobra.Command { cmd := &cobra.Command{ Use: "multi-node", Short: "Initialize config directories & files for a multi-validator testnet running locally via separate processes (e.g. Docker Compose or similar)", @@ -128,7 +127,7 @@ Example: } } - return initTestnetFiles(clientCtx, cmd, config, mbm, genBalIterator, args) + return initTestnetFiles(clientCtx, cmd, config, mbm, args) }, } @@ -164,8 +163,7 @@ func initTestnetFiles( clientCtx client.Context, cmd *cobra.Command, nodeConfig *cmtconfig.Config, - mbm module.BasicManager, - genBalIterator banktypes.GenesisBalancesIterator, + mbm *module.Manager, args initArgs, ) error { if args.chainID == "" { @@ -198,7 +196,7 @@ func initTestnetFiles( nodeConfig.SetRoot(nodeDir) nodeConfig.Moniker = nodeDirName - nodeConfig.RPC.ListenAddress = "tcp://0.0.0.0:" + args.ports[i] + nodeConfig.RPC.ListenAddress = "tcp://0.0.0.0:" + args.ports[i] var err error if err := os.MkdirAll(filepath.Join(nodeDir, "config"), nodeDirPerm); err != nil { @@ -206,7 +204,7 @@ func initTestnetFiles( return err } - nodeIDs[i], valPubKeys[i], err = genutil.InitializeNodeValidatorFiles(nodeConfig) + nodeIDs[i], valPubKeys[i], err = genutil.InitializeNodeValidatorFiles(nodeConfig, ed25519.KeyType) if err != nil { _ = os.RemoveAll(args.outputDir) return err @@ -233,7 +231,7 @@ func initTestnetFiles( return err } - addr, secret, err := testutil.GenerateSaveCoinKey(kb, nodeDirName, "", true, algo) + addr, secret, err := testutil.GenerateSaveCoinKey(kb, nodeDirName, "", true, algo, sdk.GetFullBIP44Path()) if err != nil { _ = os.RemoveAll(args.outputDir) return err @@ -293,7 +291,7 @@ func initTestnetFiles( WithKeybase(kb). WithTxConfig(clientCtx.TxConfig) - if err := tx.Sign(cmd.Context(), txFactory, nodeDirName, txBuilder, true); err != nil { + if err := tx.Sign(clientCtx, txFactory, nodeDirName, txBuilder, true); err != nil { return err } @@ -309,7 +307,10 @@ func initTestnetFiles( appConfig.GRPC.Address = args.startingIPAddress + ":" + strconv.Itoa(9090-2*i) appConfig.API.Address = "tcp://localhost:" + strconv.Itoa(1317-i) - srvconfig.WriteConfigFile(filepath.Join(nodeDir, "config", "app.toml"), appConfig) + err = srvconfig.WriteConfigFile(filepath.Join(nodeDir, "config", "app.toml"), appConfig) + if err != nil { + return err + } } if err := initGenFiles(clientCtx, mbm, args.chainID, genAccounts, genBalances, genFiles, args.numValidators); err != nil { @@ -326,13 +327,15 @@ func initTestnetFiles( if err != nil || yes { continue } - copyFile(file, gentxsDir) + _, err = copyFile(file, gentxsDir) + if err != nil { + return err + } + } } err := collectGenFiles( clientCtx, nodeConfig, nodeIDs, valPubKeys, - genBalIterator, - clientCtx.TxConfig.SigningContext().ValidatorAddressCodec(), persistentPeers, args, ) if err != nil { @@ -356,11 +359,11 @@ func writeFile(file, dir string, contents []byte) error { } func initGenFiles( - clientCtx client.Context, mbm module.BasicManager, chainID string, + clientCtx client.Context, mbm *module.Manager, chainID string, genAccounts []authtypes.GenesisAccount, genBalances []banktypes.Balance, genFiles []string, numValidators int, ) error { - appGenState := mbm.DefaultGenesis(clientCtx.Codec) + appGenState := mbm.DefaultGenesis() // set the accounts in the genesis state var authGenState authtypes.GenesisState @@ -378,7 +381,10 @@ func initGenFiles( var bankGenState banktypes.GenesisState clientCtx.Codec.MustUnmarshalJSON(appGenState[banktypes.ModuleName], &bankGenState) - bankGenState.Balances = banktypes.SanitizeGenesisBalances(genBalances) + bankGenState.Balances, err = banktypes.SanitizeGenesisBalances(genBalances, clientCtx.AddressCodec) + if err != nil { + return err + } for _, bal := range bankGenState.Balances { bankGenState.Supply = bankGenState.Supply.Add(bal.Coins...) } @@ -407,8 +413,7 @@ func initGenFiles( func collectGenFiles( clientCtx client.Context, nodeConfig *cmtconfig.Config, nodeIDs []string, valPubKeys []cryptotypes.PubKey, - genBalIterator banktypes.GenesisBalancesIterator, - valAddrCodec runtime.ValidatorAddressCodec, persistentPeers string, + persistentPeers string, args initArgs, ) error { chainID := args.chainID @@ -435,8 +440,8 @@ func collectGenFiles( return err } - nodeAppState, err := genutil.GenAppStateFromConfig(clientCtx.Codec, clientCtx.TxConfig, nodeConfig, initCfg, appGenesis, genBalIterator, genutiltypes.DefaultMessageValidator, - valAddrCodec) + nodeAppState, err := genutil.GenAppStateFromConfig(clientCtx.Codec, clientCtx.TxConfig, nodeConfig, initCfg, appGenesis, genutiltypes.DefaultMessageValidator, + clientCtx.ValidatorAddressCodec, clientCtx.AddressCodec) if err != nil { return err }