Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add multi node (validator) testnet #4377

Open
wants to merge 40 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
3e8a83b
add cmd multi-node
likesToEatFish Sep 28, 2024
3e53b99
read config
likesToEatFish Sep 28, 2024
dfb928e
minor
likesToEatFish Sep 28, 2024
df91385
init node
likesToEatFish Sep 28, 2024
48d336d
done
likesToEatFish Sep 29, 2024
d492d08
remove try
likesToEatFish Sep 29, 2024
1fdea3e
updates
likesToEatFish Sep 29, 2024
4a7c50d
updates
likesToEatFish Sep 29, 2024
982e454
make format
likesToEatFish Sep 29, 2024
06f65b7
updates
likesToEatFish Sep 30, 2024
4f4bada
minor
likesToEatFish Sep 30, 2024
3a2f656
rename and add info node
likesToEatFish Sep 30, 2024
fa09a75
add a reset command
likesToEatFish Sep 30, 2024
1daa0c3
changelog
likesToEatFish Oct 1, 2024
dedd592
lint
likesToEatFish Oct 1, 2024
c2fb623
use config.yml with more validators
likesToEatFish Oct 2, 2024
05b7822
updates docs
likesToEatFish Oct 2, 2024
5b8b7f7
show log
likesToEatFish Oct 5, 2024
0acff49
Merge branch 'main' into duong/multi-node
likesToEatFish Oct 6, 2024
e7a3341
rename package model to bubblemodel
likesToEatFish Oct 7, 2024
3a2da5f
Update ignite/cmd/testnet_multi_node.go
likesToEatFish Oct 9, 2024
e86b9aa
nits
likesToEatFish Oct 9, 2024
911b112
Merge branch 'main' into duong/multi-node
Pantani Oct 11, 2024
8f24f18
Update ignite/cmd/bubblemodel/testnet_multi_node.go
likesToEatFish Oct 13, 2024
176b342
Update ignite/cmd/bubblemodel/testnet_multi_node.go
likesToEatFish Oct 13, 2024
5c8593a
Update ignite/cmd/bubblemodel/testnet_multi_node.go
likesToEatFish Oct 13, 2024
bf286de
Update ignite/cmd/testnet_multi_node.go
likesToEatFish Oct 13, 2024
748fab8
updates
likesToEatFish Oct 13, 2024
c89e7f4
use lipgloss for View
likesToEatFish Oct 13, 2024
b26b894
status bar
likesToEatFish Oct 14, 2024
080f811
nits
likesToEatFish Oct 14, 2024
b25b9d4
Update ignite/cmd/bubblemodel/testnet_multi_node.go
likesToEatFish Oct 14, 2024
697e05c
Update ignite/cmd/bubblemodel/testnet_multi_node.go
likesToEatFish Oct 14, 2024
28d9df7
remove ctx
likesToEatFish Oct 14, 2024
6a9a4f4
add comment
likesToEatFish Oct 14, 2024
67d3e88
use ports in ignite/pkg/availableport/availableport.go
likesToEatFish Oct 16, 2024
2b0fc01
update errgroup
likesToEatFish Oct 16, 2024
c747020
merge main
likesToEatFish Oct 16, 2024
e9e8dd2
Update changelog.md
likesToEatFish Oct 20, 2024
7e24538
updates with v0.52
likesToEatFish Oct 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
162 changes: 162 additions & 0 deletions ignite/cmd/model/testnet_multi_node.go
likesToEatFish marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
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

Check failure on line 42 in ignite/cmd/model/testnet_multi_node.go

View workflow job for this annotation

GitHub Actions / Lint Go code

Comment should end in a period (godot)
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

Check failure on line 58 in ignite/cmd/model/testnet_multi_node.go

View workflow job for this annotation

GitHub Actions / Lint Go code

Comment should end in a period (godot)
func (m Model) Init() tea.Cmd {
return nil
}

// ToggleNode toggles the state of a node

Check failure on line 63 in ignite/cmd/model/testnet_multi_node.go

View workflow job for this annotation

GitHub Actions / Lint Go code

Comment should end in a period (godot)
func ToggleNode(nodeIdx int) tea.Cmd {
return func() tea.Msg {
return ToggleNodeMsg{nodeIdx: nodeIdx}
}
}

// Run or stop the node based on its status

Check failure on line 70 in ignite/cmd/model/testnet_multi_node.go

View workflow job for this annotation

GitHub Actions / Lint Go code

Comment should end in a period (godot)
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
likesToEatFish marked this conversation as resolved.
Show resolved Hide resolved
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

Check failure on line 90 in ignite/cmd/model/testnet_multi_node.go

View workflow job for this annotation

GitHub Actions / Lint Go code

Error return value of `cmd.Wait` is not checked (errcheck)
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

Check failure on line 107 in ignite/cmd/model/testnet_multi_node.go

View workflow job for this annotation

GitHub Actions / Lint Go code

Comment should end in a period (godot)
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

Check failure on line 116 in ignite/cmd/model/testnet_multi_node.go

View workflow job for this annotation

GitHub Actions / Lint Go code

Comment should end in a period (godot)
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

Check failure on line 147 in ignite/cmd/model/testnet_multi_node.go

View workflow job for this annotation

GitHub Actions / Lint Go code

Comment should end in a period (godot)
func (m Model) View() string {
statusText := func(status NodeStatus) string {
if status == Running {
return "[Running]"
}
return "[Stopped]"
}

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 += "Press q to quit.\n"
return output
}
1 change: 1 addition & 0 deletions ignite/cmd/testnet.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ func NewTestnet() *cobra.Command {

c.AddCommand(
NewTestnetInPlace(),
NewTestnetMultiNode(),
)

return c
Expand Down
175 changes: 175 additions & 0 deletions ignite/cmd/testnet_multi_node.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
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"
)

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
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


`,
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())
likesToEatFish marked this conversation as resolved.
Show resolved Hide resolved

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.MultiNode)
if err != nil {
return err
}
args := chain.MultiNodeArgs{
ChainID: cfg.MultiNode.ChainID,
ValidatorsStakeAmount: amountDetails,
OutputDir: cfg.MultiNode.OutputDir,
NumValidator: strconv.Itoa(numVal),
NodeDirPrefix: cfg.MultiNode.NodeDirPrefix,
}

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()
likesToEatFish marked this conversation as resolved.
Show resolved Hide resolved

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

Check failure on line 131 in ignite/cmd/testnet_multi_node.go

View workflow job for this annotation

GitHub Actions / Lint Go code

Comment should end in a period (godot)
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 count, amounts, err
}
maxStake, err := sdk.ParseCoinNormalized(cfg.RandomValidators.MaxStake)
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)

Check failure on line 149 in ignite/cmd/testnet_multi_node.go

View workflow job for this annotation

GitHub Actions / Lint Go code

G404: Use of weak random number generator (math/rand or math/rand/v2 instead of crypto/rand) (gosec)
if amounts == "" {
amounts = math.NewIntFromUint64(stakeAmount).String()
count++
} else {
amounts = amounts + "," + math.NewIntFromUint64(stakeAmount).String()
count++
}
}
} 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 += 1
} else {
amounts = amounts + "," + stakeAmount.Amount.String()
count += 1
}
}
}

return count, amounts, nil
}
23 changes: 23 additions & 0 deletions ignite/config/chain/base/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,29 @@ 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."`
likesToEatFish marked this conversation as resolved.
Show resolved Hide resolved
OutputDir string `yaml:"output-dir" doc:"Directory to store initialization data for the testnet"`
likesToEatFish marked this conversation as resolved.
Show resolved Hide resolved
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.
Expand Down
4 changes: 4 additions & 0 deletions ignite/pkg/chaincmd/chaincmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const (
commandExport = "export"
commandTendermint = "tendermint"
commandTestnetInPlace = "in-place-testnet"
commandTestnetMultiNode = "multi-node"

optionHome = "--home"
optionNode = "--node"
Expand Down Expand Up @@ -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"
Expand Down
Loading
Loading