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/internal/p2p"
|
|
"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)
|
|
|
|
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(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(context.TODO(), 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(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.AddrBookStrict = false
|
|
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)
|
|
|
|
cfg.WriteConfigFile(nodeDir, config)
|
|
}
|
|
|
|
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 := p2p.LoadNodeKey(config.NodeKeyFile())
|
|
if err != nil {
|
|
return []string{}, err
|
|
}
|
|
peers[i] = nodeKey.ID.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()
|
|
}
|