package commands import ( "context" "fmt" "net" "os" "path/filepath" "strings" "github.com/spf13/cobra" "github.com/spf13/viper" cfg "github.com/tendermint/tendermint/config" "github.com/tendermint/tendermint/libs/bytes" tmrand "github.com/tendermint/tendermint/libs/rand" tmtime "github.com/tendermint/tendermint/libs/time" "github.com/tendermint/tendermint/privval" "github.com/tendermint/tendermint/types" ) var ( nValidators int nNonValidators int initialHeight int64 configFile string outputDir string nodeDirPrefix string populatePersistentPeers bool hostnamePrefix string hostnameSuffix string startingIPAddress string hostnames []string p2pPort int randomMonikers bool ) const ( nodeDirPerm = 0755 ) func init() { TestnetFilesCmd.Flags().IntVar(&nValidators, "v", 4, "number of validators to initialize the testnet with") TestnetFilesCmd.Flags().StringVar(&configFile, "config", "", "config file to use (note some options may be overwritten)") TestnetFilesCmd.Flags().IntVar(&nNonValidators, "n", 0, "number of non-validators to initialize the testnet with") TestnetFilesCmd.Flags().StringVar(&outputDir, "o", "./mytestnet", "directory to store initialization data for the testnet") TestnetFilesCmd.Flags().StringVar(&nodeDirPrefix, "node-dir-prefix", "node", "prefix the directory name for each node with (node results in node0, node1, ...)") TestnetFilesCmd.Flags().Int64Var(&initialHeight, "initial-height", 0, "initial height of the first block") TestnetFilesCmd.Flags().BoolVar(&populatePersistentPeers, "populate-persistent-peers", true, "update config of each node with the list of persistent peers build using either"+ " hostname-prefix or"+ " starting-ip-address") TestnetFilesCmd.Flags().StringVar(&hostnamePrefix, "hostname-prefix", "node", "hostname prefix (\"node\" results in persistent peers list ID0@node0:26656, ID1@node1:26656, ...)") TestnetFilesCmd.Flags().StringVar(&hostnameSuffix, "hostname-suffix", "", "hostname suffix ("+ "\".xyz.com\""+ " results in persistent peers list ID0@node0.xyz.com:26656, ID1@node1.xyz.com:26656, ...)") TestnetFilesCmd.Flags().StringVar(&startingIPAddress, "starting-ip-address", "", "starting IP address ("+ "\"192.168.0.1\""+ " results in persistent peers list ID0@192.168.0.1:26656, ID1@192.168.0.2:26656, ...)") TestnetFilesCmd.Flags().StringArrayVar(&hostnames, "hostname", []string{}, "manually override all hostnames of validators and non-validators (use --hostname multiple times for multiple hosts)") TestnetFilesCmd.Flags().IntVar(&p2pPort, "p2p-port", 26656, "P2P Port") TestnetFilesCmd.Flags().BoolVar(&randomMonikers, "random-monikers", false, "randomize the moniker for each generated node") TestnetFilesCmd.Flags().StringVar(&keyType, "key", types.ABCIPubKeyTypeEd25519, "Key type to generate privval file with. Options: ed25519, secp256k1") } // TestnetFilesCmd allows initialisation of files for a Tendermint testnet. var TestnetFilesCmd = &cobra.Command{ Use: "testnet", Short: "Initialize files for a Tendermint testnet", Long: `testnet will create "v" + "n" number of directories and populate each with necessary files (private validator, genesis, config, etc.). Note, strict routability for addresses is turned off in the config file. Optionally, it will fill in persistent-peers list in config file using either hostnames or IPs. Example: tendermint testnet --v 4 --o ./output --populate-persistent-peers --starting-ip-address 192.168.10.2 `, RunE: testnetFiles, } func testnetFiles(cmd *cobra.Command, args []string) error { if len(hostnames) > 0 && len(hostnames) != (nValidators+nNonValidators) { return fmt.Errorf( "testnet needs precisely %d hostnames (number of validators plus non-validators) if --hostname parameter is used", nValidators+nNonValidators, ) } // set mode to validator for testnet config := cfg.DefaultValidatorConfig() // overwrite default config if set and valid if configFile != "" { viper.SetConfigFile(configFile) if err := viper.ReadInConfig(); err != nil { return err } if err := viper.Unmarshal(config); err != nil { return err } if err := config.ValidateBasic(); err != nil { return err } } genVals := make([]types.GenesisValidator, nValidators) ctx := cmd.Context() for i := 0; i < nValidators; i++ { nodeDirName := fmt.Sprintf("%s%d", nodeDirPrefix, i) nodeDir := filepath.Join(outputDir, nodeDirName) config.SetRoot(nodeDir) err := os.MkdirAll(filepath.Join(nodeDir, "config"), nodeDirPerm) if err != nil { _ = os.RemoveAll(outputDir) return err } err = os.MkdirAll(filepath.Join(nodeDir, "data"), nodeDirPerm) if err != nil { _ = os.RemoveAll(outputDir) return err } if err := initFilesWithConfig(ctx, config); err != nil { return err } pvKeyFile := filepath.Join(nodeDir, config.PrivValidator.Key) pvStateFile := filepath.Join(nodeDir, config.PrivValidator.State) pv, err := privval.LoadFilePV(pvKeyFile, pvStateFile) if err != nil { return err } ctx, cancel := context.WithTimeout(ctx, ctxTimeout) defer cancel() pubKey, err := pv.GetPubKey(ctx) if err != nil { return fmt.Errorf("can't get pubkey: %w", err) } genVals[i] = types.GenesisValidator{ Address: pubKey.Address(), PubKey: pubKey, Power: 1, Name: nodeDirName, } } for i := 0; i < nNonValidators; i++ { nodeDir := filepath.Join(outputDir, fmt.Sprintf("%s%d", nodeDirPrefix, i+nValidators)) config.SetRoot(nodeDir) err := os.MkdirAll(filepath.Join(nodeDir, "config"), nodeDirPerm) if err != nil { _ = os.RemoveAll(outputDir) return err } err = os.MkdirAll(filepath.Join(nodeDir, "data"), nodeDirPerm) if err != nil { _ = os.RemoveAll(outputDir) return err } if err := initFilesWithConfig(ctx, config); err != nil { return err } } // Generate genesis doc from generated validators genDoc := &types.GenesisDoc{ ChainID: "chain-" + tmrand.Str(6), GenesisTime: tmtime.Now(), InitialHeight: initialHeight, Validators: genVals, ConsensusParams: types.DefaultConsensusParams(), } if keyType == "secp256k1" { genDoc.ConsensusParams.Validator = types.ValidatorParams{ PubKeyTypes: []string{types.ABCIPubKeyTypeSecp256k1}, } } // Write genesis file. for i := 0; i < nValidators+nNonValidators; i++ { nodeDir := filepath.Join(outputDir, fmt.Sprintf("%s%d", nodeDirPrefix, i)) if err := genDoc.SaveAs(filepath.Join(nodeDir, config.BaseConfig.Genesis)); err != nil { _ = os.RemoveAll(outputDir) return err } } // Gather persistent peer addresses. var ( persistentPeers = make([]string, 0) err error ) if populatePersistentPeers { persistentPeers, err = persistentPeersArray(config) if err != nil { _ = os.RemoveAll(outputDir) return err } } // Overwrite default config. for i := 0; i < nValidators+nNonValidators; i++ { nodeDir := filepath.Join(outputDir, fmt.Sprintf("%s%d", nodeDirPrefix, i)) config.SetRoot(nodeDir) config.P2P.AllowDuplicateIP = true if populatePersistentPeers { persistentPeersWithoutSelf := make([]string, 0) for j := 0; j < len(persistentPeers); j++ { if j == i { continue } persistentPeersWithoutSelf = append(persistentPeersWithoutSelf, persistentPeers[j]) } config.P2P.PersistentPeers = strings.Join(persistentPeersWithoutSelf, ",") } config.Moniker = moniker(i) if err := cfg.WriteConfigFile(nodeDir, config); err != nil { return err } } fmt.Printf("Successfully initialized %v node directories\n", nValidators+nNonValidators) return nil } func hostnameOrIP(i int) string { if len(hostnames) > 0 && i < len(hostnames) { return hostnames[i] } if startingIPAddress == "" { return fmt.Sprintf("%s%d%s", hostnamePrefix, i, hostnameSuffix) } ip := net.ParseIP(startingIPAddress) ip = ip.To4() if ip == nil { fmt.Printf("%v: non ipv4 address\n", startingIPAddress) os.Exit(1) } for j := 0; j < i; j++ { ip[3]++ } return ip.String() } // get an array of persistent peers func persistentPeersArray(config *cfg.Config) ([]string, error) { peers := make([]string, nValidators+nNonValidators) for i := 0; i < nValidators+nNonValidators; i++ { nodeDir := filepath.Join(outputDir, fmt.Sprintf("%s%d", nodeDirPrefix, i)) config.SetRoot(nodeDir) nodeKey, err := config.LoadNodeKeyID() if err != nil { return []string{}, err } peers[i] = nodeKey.AddressString(fmt.Sprintf("%s:%d", hostnameOrIP(i), p2pPort)) } return peers, nil } func moniker(i int) string { if randomMonikers { return randomMoniker() } if len(hostnames) > 0 && i < len(hostnames) { return hostnames[i] } if startingIPAddress == "" { return fmt.Sprintf("%s%d%s", hostnamePrefix, i, hostnameSuffix) } return randomMoniker() } func randomMoniker() string { return bytes.HexBytes(tmrand.Bytes(8)).String() }