diff --git a/changelog.md b/changelog.md index b6a35e7906..708e79f889 100644 --- a/changelog.md +++ b/changelog.md @@ -18,6 +18,7 @@ - [#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 +- [#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` - [#4289](https://github.com/ignite/cli/pull/4289) Cosmos SDK v0.52 support 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/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/bubblemodel/testnet_multi_node.go b/ignite/cmd/bubblemodel/testnet_multi_node.go new file mode 100644 index 0000000000..0ae5eb14c4 --- /dev/null +++ b/ignite/cmd/bubblemodel/testnet_multi_node.go @@ -0,0 +1,269 @@ +package cmdmodel + +import ( + "bufio" + "context" + "fmt" + "os/exec" + "path/filepath" + "strconv" + "syscall" + + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "golang.org/x/sync/errgroup" + + "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 { + ctx context.Context + appd string + args chain.MultiNodeArgs + + nodeStatuses []NodeStatus + pids []int // Store the PIDs of the running processes + numNodes int // Number of nodes + 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 +} + +// 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{} + +// 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{} + } +} + +// NewModel initializes the model. +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 + pids: make([]int, numNodes), + numNodes: numNodes, + logs: make([][]string, numNodes), // Initialize logs for each node + }, nil +} + +// Init implements the Init method of the tea.Model interface. +func (m MultiNode) 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} + } +} + +// 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] + args = m.args + appd = m.appd + ) + + return func() tea.Msg { + if start { + nodeHome := filepath.Join(args.OutputDir, args.NodeDirPrefix+strconv.Itoa(nodeIdx)) + // Create the command to run in the 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 + } + + 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} + } + + *pid = cmd.Process.Pid // Store the PID + + // Create an errgroup with context + g, gCtx := errgroup.WithContext(m.ctx) + g.Go(func() error { + scanner := bufio.NewScanner(stdout) + for scanner.Scan() { + 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 + 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} + } +} + +// StopAllNodes stops all nodes. +func (m *MultiNode) StopAllNodes() { + for i := 0; i < m.numNodes; i++ { + if m.nodeStatuses[i] == Running { + RunNode(i, false, *m)() // Stop node + } + } +} + +// 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: + 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) // Stop node + } + return m, RunNode(msg.nodeIdx, true, m) // Start node + + case UpdateStatusMsg: + m.nodeStatuses[msg.nodeIdx] = msg.status + return m, UpdateDeemon() + case UpdateLogsMsg: + return m, UpdateDeemon() + } + + return m, nil +} + +// 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().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 + output += purpleStyle.Render("Node Control:") + for i := 0; i < m.numNodes; i++ { + status := stoppedStyle.Render("[Stopped]") + if m.nodeStatuses[i] == Running { + status = runningStyle.Render("[Running]") + } + + 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)) + + output += fmt.Sprintf("\n%s Node %d %s %s %s:\n", nodeNumber, i+1, status, nodeGray, tcpAddress) + output += " [\n" + if m.logs != nil { + for _, line := range m.logs[i] { + output += " " + line + "\n" + } + } + + output += " ]\n\n" + } + + output += grayStyle.Render("\nPress q to quit.\n") + return output +} 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.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..0a3db23689 --- /dev/null +++ b/ignite/cmd/testnet_multi_node.go @@ -0,0 +1,171 @@ +package ignitecmd + +import ( + "os" + "strconv" + "time" + + tea "github.com/charmbracelet/bubbletea" + 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" + 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" +) + +const ( + flagNodeDirPrefix = "node-dir-prefix" +) + +func NewTestnetMultiNode() *cobra.Command { + c := &cobra.Command{ + Use: "multi-node", + 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:: + ... + 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, + } + flagSetPath(c) + flagSetClearCache(c) + c.Flags().AddFlagSet(flagSetHome()) + 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().String(flagNodeDirPrefix, "validator", "prefix of dir node") + + 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() + + return testnetMultiNode(cmd, session) +} + +func testnetMultiNode(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 + } + + numVal, amountDetails, err := getValidatorAmountStake(cfg.Validators) + if err != nil { + return err + } + nodeDirPrefix, _ := cmd.Flags().GetString(flagNodeDirPrefix) + + outputDir, err := xfilepath.Join(igcfg.DirPath, xfilepath.Path("local-chains/"+c.Name()+"d/"+"testnet/"))() + 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) + 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 + } + + time.Sleep(2 * time.Second) + + model, err := cmdmodel.NewModel(cmd.Context(), c.Name(), args) + if err != nil { + return err + } + + _, err = tea.NewProgram(model).Run() + return err +} + +// getValidatorAmountStake returns the number of validators and the amountStakes arg from config.MultiNode. +func getValidatorAmountStake(validators []v1.Validator) (int, string, error) { + numVal := len(validators) + var amounts string + + for _, v := range validators { + stakeAmount, err := sdk.ParseCoinNormalized(v.Bonded) + if err != nil { + return numVal, amounts, err + } + if amounts == "" { + amounts = stakeAmount.Amount.String() + } else { + amounts = amounts + "," + stakeAmount.Amount.String() + } + } + + return numVal, amounts, nil +} 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/chaincmd.go b/ignite/pkg/chaincmd/chaincmd.go index 6c91b49318..21f1682707 100644 --- a/ignite/pkg/chaincmd/chaincmd.go +++ b/ignite/pkg/chaincmd/chaincmd.go @@ -28,6 +28,7 @@ const ( commandUnsafeReset = "unsafe-reset-all" commandTendermint = "tendermint" commandTestnetInPlace = "in-place-testnet" + commandTestnetMultiNode = "multi-node" optionHome = "--home" optionNode = "--node" @@ -59,6 +60,11 @@ const ( optionValidatorPrivateKey = "--validator-privkey" optionAccountToFund = "--accounts-to-fund" optionSkipConfirmation = "--skip-confirmation" + optionAmountStakes = "--validators-stake-amount" + 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 84a3b38465..10313c11fe 100644 --- a/ignite/pkg/chaincmd/in-place-testnet.go +++ b/ignite/pkg/chaincmd/in-place-testnet.go @@ -45,3 +45,84 @@ 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 { + return append(s, optionChainID, chainID) + } + return s + } +} + +// 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 { + return append(s, optionOutPutDir, dirOutput) + } + return s + } +} + +// 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 { + return append(s, optionNumValidator, numVal) + } + return s + } +} + +// 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 { + return append(s, optionAmountStakes, satkeAmounts) + } + return s + } +} + +// 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 { + return append(s, optionNodeDirPrefix, nodeDirPrefix) + } + return s + } +} + +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{ + commandTestnetMultiNode, + } + + // Apply the options provided by the user + for _, apply := range options { + command = apply(command) + } + + return c.daemonCommand(command) +} diff --git a/ignite/pkg/chaincmd/runner/chain.go b/ignite/pkg/chaincmd/runner/chain.go index 0b133a47d2..4130d01312 100644 --- a/ignite/pkg/chaincmd/runner/chain.go +++ b/ignite/pkg/chaincmd/runner/chain.go @@ -57,6 +57,19 @@ 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, + 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/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 diff --git a/ignite/services/chain/runtime.go b/ignite/services/chain/runtime.go index a0eb21e1df..15af380016 100644 --- a/ignite/services/chain/runtime.go +++ b/ignite/services/chain/runtime.go @@ -48,6 +48,18 @@ 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.MultiNodeWithDirOutput(args.OutputDir), + chaincmd.MultiNodeWithNumValidator(args.NumValidator), + chaincmd.MultiNodeWithValidatorsStakeAmount(args.ValidatorsStakeAmount), + chaincmd.MultiNodeDirPrefix(args.NodeDirPrefix), + chaincmd.MultiNodePorts(args.ConvertPorts()), + ) + 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..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" ) @@ -35,3 +37,45 @@ func (c Chain) TestnetInPlace(ctx context.Context, args InPlaceArgs) error { } return nil } + +type MultiNodeArgs struct { + OutputDir string + 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. +// 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 { + 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/commands.go.plush b/ignite/templates/app/files/cmd/{{binaryNamePrefix}}d/cmd/commands.go.plush index 05aaaabc8f..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 @@ -36,6 +36,7 @@ func initRootCmd( rootCmd.AddCommand( genutilcli.InitCmd(moduleManager), NewInPlaceTestnetCmd(), + 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 new file mode 100644 index 0000000000..0f6532b750 --- /dev/null +++ b/ignite/templates/app/files/cmd/{{binaryNamePrefix}}d/cmd/testnet_multi_node.go.plush @@ -0,0 +1,542 @@ +package cmd + +import ( + "bufio" + "encoding/json" + "fmt" + "io" + "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 "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" + "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" +) + +var ( + flagNodeDirPrefix = "node-dir-prefix" + flagPorts = "list-ports" + 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 + ports map[int]string +} + +// NewTestnetMultiNodeCmd returns a cmd to initialize all files for tendermint testnet and application +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)", + 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: + <%= 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) + 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.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. + 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 + } + + } + 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, 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, ...)") + 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.Manager, + args initArgs, +) error { + if args.chainID == "" { + args.chainID = "chain-" + generateRandomString(6) + } + nodeIDs := make([]string, args.numValidators) + valPubKeys := make([]cryptotypes.PubKey, args.numValidators) + + 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 + genBalances []banktypes.Balance + genFiles []string + persistentPeers string + gentxsFiles []string + ) + + inBuf := bufio.NewReader(cmd.InOrStdin()) + for i := 0; i < args.numValidators; i++ { + nodeDirName := fmt.Sprintf("%s%d", args.nodeDirPrefix, i) + nodeDir := filepath.Join(args.outputDir, nodeDirName) + gentxsDir := filepath.Join(args.outputDir, nodeDirName, "config", "gentx") + + nodeConfig.SetRoot(nodeDir) + nodeConfig.Moniker = nodeDirName + 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 { + _ = os.RemoveAll(args.outputDir) + return err + } + + nodeIDs[i], valPubKeys[i], err = genutil.InitializeNodeValidatorFiles(nodeConfig, ed25519.KeyType) + 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, sdk.GetFullBIP44Path()) + 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(clientCtx, 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 + } + + appConfig.GRPC.Address = args.startingIPAddress + ":" + strconv.Itoa(9090-2*i) + appConfig.API.Address = "tcp://localhost:" + strconv.Itoa(1317-i) + 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 { + return err + } + // copy gentx file + 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 + } + _, err = copyFile(file, gentxsDir) + if err != nil { + return err + } + + } + } + err := collectGenFiles( + clientCtx, nodeConfig, nodeIDs, valPubKeys, + persistentPeers, args, + ) + 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.Manager, chainID string, + genAccounts []authtypes.GenesisAccount, genBalances []banktypes.Balance, + genFiles []string, numValidators int, +) error { + appGenState := mbm.DefaultGenesis() + + // 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, err = banktypes.SanitizeGenesisBalances(genBalances, clientCtx.AddressCodec) + if err != nil { + return err + } + 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, + nodeIDs []string, valPubKeys []cryptotypes.PubKey, + persistentPeers string, + args initArgs, +) error { + chainID := args.chainID + numValidators := args.numValidators + outputDir := args.outputDir + nodeDirPrefix := args.nodeDirPrefix + + 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, genutiltypes.DefaultMessageValidator, + clientCtx.ValidatorAddressCodec, clientCtx.AddressCodec) + 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:" + 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 + 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 +} + +// 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) +} diff --git a/ignite/templates/app/files/config.yml.plush b/ignite/templates/app/files/config.yml.plush index 1bb23a6518..e6c00d4729 100644 --- a/ignite/templates/app/files/config.yml.plush +++ b/ignite/templates/app/files/config.yml.plush @@ -20,3 +20,9 @@ faucet: validators: - name: alice bonded: 100000000stake +- name: validator1 + bonded: 100000000stake +- name: validator2 + bonded: 200000000stake +- name: validator3 + bonded: 300000000stake \ No newline at end of file