github.com/badrootd/celestia-core@v0.0.0-20240305091328-aa4207a4b25d/test/e2e/pkg/testnet.go (about)

     1  package e2e
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"io"
     7  	"math/rand"
     8  	"net"
     9  	"path/filepath"
    10  	"sort"
    11  	"strconv"
    12  	"strings"
    13  
    14  	"github.com/badrootd/celestia-core/config"
    15  	"github.com/badrootd/celestia-core/crypto"
    16  	"github.com/badrootd/celestia-core/crypto/ed25519"
    17  	"github.com/badrootd/celestia-core/crypto/secp256k1"
    18  	rpchttp "github.com/badrootd/celestia-core/rpc/client/http"
    19  	mcs "github.com/badrootd/celestia-core/test/maverick/consensus"
    20  )
    21  
    22  const (
    23  	randomSeed               int64  = 2308084734268
    24  	proxyPortFirst           uint32 = 5701
    25  	prometheusProxyPortFirst uint32 = 6701
    26  
    27  	defaultBatchSize   = 2
    28  	defaultConnections = 1
    29  	defaultTxSizeBytes = 1024
    30  
    31  	localVersion = "cometbft/e2e-node:local-version"
    32  )
    33  
    34  type (
    35  	Mode         string
    36  	Protocol     string
    37  	Perturbation string
    38  )
    39  
    40  const (
    41  	ModeValidator Mode = "validator"
    42  	ModeFull      Mode = "full"
    43  	ModeLight     Mode = "light"
    44  	ModeSeed      Mode = "seed"
    45  
    46  	ProtocolBuiltin Protocol = "builtin"
    47  	ProtocolFile    Protocol = "file"
    48  	ProtocolGRPC    Protocol = "grpc"
    49  	ProtocolTCP     Protocol = "tcp"
    50  	ProtocolUNIX    Protocol = "unix"
    51  
    52  	PerturbationDisconnect Perturbation = "disconnect"
    53  	PerturbationKill       Perturbation = "kill"
    54  	PerturbationPause      Perturbation = "pause"
    55  	PerturbationRestart    Perturbation = "restart"
    56  	PerturbationUpgrade    Perturbation = "upgrade"
    57  )
    58  
    59  // Testnet represents a single testnet.
    60  type Testnet struct {
    61  	Name                   string
    62  	File                   string
    63  	Dir                    string
    64  	IP                     *net.IPNet
    65  	InitialHeight          int64
    66  	InitialState           map[string]string
    67  	Validators             map[*Node]int64
    68  	ValidatorUpdates       map[int64]map[*Node]int64
    69  	Nodes                  []*Node
    70  	KeyType                string
    71  	Evidence               int
    72  	LoadTxSizeBytes        int
    73  	MaxInboundConnections  int
    74  	MaxOutboundConnections int
    75  	LoadTxBatchSize        int
    76  	LoadTxConnections      int
    77  	ABCIProtocol           string
    78  	UpgradeVersion         string
    79  	Prometheus             bool
    80  }
    81  
    82  // Node represents a CometBFT node in a testnet.
    83  type Node struct {
    84  	Name                  string
    85  	Version               string
    86  	Testnet               *Testnet
    87  	Mode                  Mode
    88  	PrivvalKey            crypto.PrivKey
    89  	NodeKey               crypto.PrivKey
    90  	IP                    net.IP
    91  	ProxyPort             uint32
    92  	StartAt               int64
    93  	FastSync              string
    94  	StateSync             bool
    95  	Mempool               string
    96  	Database              string
    97  	ABCIProtocol          Protocol
    98  	PrivvalProtocol       Protocol
    99  	PersistInterval       uint64
   100  	SnapshotInterval      uint64
   101  	RetainBlocks          uint64
   102  	Seeds                 []*Node
   103  	PersistentPeers       []*Node
   104  	Perturbations         []Perturbation
   105  	Misbehaviors          map[int64]string
   106  	SendNoLoad            bool
   107  	Prometheus            bool
   108  	PrometheusProxyPort   uint32
   109  	InfluxDBURL           string
   110  	InfluxDBToken         string
   111  	PyroscopeURL          string
   112  	PyroscopeTrace        bool
   113  	PyroscopeProfileTypes string
   114  }
   115  
   116  // LoadTestnet loads a testnet from a manifest file, using the filename to
   117  // determine the testnet name and directory (from the basename of the file).
   118  // The testnet generation must be deterministic, since it is generated
   119  // separately by the runner and the test cases. For this reason, testnets use a
   120  // random seed to generate e.g. keys.
   121  func LoadTestnet(manifest Manifest, fname string, ifd InfrastructureData) (*Testnet, error) {
   122  	dir := strings.TrimSuffix(fname, filepath.Ext(fname))
   123  	keyGen := newKeyGenerator(randomSeed)
   124  	proxyPortGen := newPortGenerator(proxyPortFirst)
   125  	prometheusProxyPortGen := newPortGenerator(prometheusProxyPortFirst)
   126  	_, ipNet, err := net.ParseCIDR(ifd.Network)
   127  	if err != nil {
   128  		return nil, fmt.Errorf("invalid IP network address %q: %w", ifd.Network, err)
   129  	}
   130  
   131  	testnet := &Testnet{
   132  		Name:                   filepath.Base(dir),
   133  		File:                   fname,
   134  		Dir:                    dir,
   135  		IP:                     ipNet,
   136  		InitialHeight:          1,
   137  		InitialState:           manifest.InitialState,
   138  		Validators:             map[*Node]int64{},
   139  		ValidatorUpdates:       map[int64]map[*Node]int64{},
   140  		Nodes:                  []*Node{},
   141  		MaxInboundConnections:  manifest.MaxInboundConnections,
   142  		MaxOutboundConnections: manifest.MaxOutboundConnections,
   143  		LoadTxSizeBytes:        manifest.LoadTxSizeBytes,
   144  		LoadTxBatchSize:        manifest.LoadTxBatchSize,
   145  		LoadTxConnections:      manifest.LoadTxConnections,
   146  		ABCIProtocol:           manifest.ABCIProtocol,
   147  		UpgradeVersion:         manifest.UpgradeVersion,
   148  		Prometheus:             manifest.Prometheus,
   149  	}
   150  	if len(manifest.KeyType) != 0 {
   151  		testnet.KeyType = manifest.KeyType
   152  	}
   153  	if manifest.InitialHeight > 0 {
   154  		testnet.InitialHeight = manifest.InitialHeight
   155  	}
   156  	if testnet.ABCIProtocol == "" {
   157  		testnet.ABCIProtocol = string(ProtocolBuiltin)
   158  	}
   159  	if testnet.UpgradeVersion == "" {
   160  		testnet.UpgradeVersion = localVersion
   161  	}
   162  	if testnet.LoadTxConnections == 0 {
   163  		testnet.LoadTxConnections = defaultConnections
   164  	}
   165  	if testnet.LoadTxBatchSize == 0 {
   166  		testnet.LoadTxBatchSize = defaultBatchSize
   167  	}
   168  	if testnet.LoadTxSizeBytes == 0 {
   169  		testnet.LoadTxSizeBytes = defaultTxSizeBytes
   170  	}
   171  
   172  	// Set up nodes, in alphabetical order (IPs and ports get same order).
   173  	nodeNames := []string{}
   174  	for name := range manifest.Nodes {
   175  		nodeNames = append(nodeNames, name)
   176  	}
   177  	sort.Strings(nodeNames)
   178  
   179  	for _, name := range nodeNames {
   180  		nodeManifest := manifest.Nodes[name]
   181  		ind, ok := ifd.Instances[name]
   182  		if !ok {
   183  			return nil, fmt.Errorf("information for node '%s' missing from infrastructure data", name)
   184  		}
   185  		v := nodeManifest.Version
   186  		if v == "" {
   187  			v = localVersion
   188  		}
   189  
   190  		node := &Node{
   191  			Name:                  name,
   192  			Version:               v,
   193  			Testnet:               testnet,
   194  			PrivvalKey:            keyGen.Generate(manifest.KeyType),
   195  			NodeKey:               keyGen.Generate("ed25519"),
   196  			IP:                    ind.IPAddress,
   197  			ProxyPort:             proxyPortGen.Next(),
   198  			Mode:                  ModeValidator,
   199  			Database:              "goleveldb",
   200  			ABCIProtocol:          Protocol(testnet.ABCIProtocol),
   201  			PrivvalProtocol:       ProtocolFile,
   202  			StartAt:               nodeManifest.StartAt,
   203  			FastSync:              nodeManifest.FastSync,
   204  			Mempool:               nodeManifest.Mempool,
   205  			StateSync:             nodeManifest.StateSync,
   206  			PersistInterval:       1,
   207  			SnapshotInterval:      nodeManifest.SnapshotInterval,
   208  			RetainBlocks:          nodeManifest.RetainBlocks,
   209  			Perturbations:         []Perturbation{},
   210  			Misbehaviors:          make(map[int64]string),
   211  			SendNoLoad:            nodeManifest.SendNoLoad,
   212  			InfluxDBURL:           ifd.InfluxDBURL,
   213  			InfluxDBToken:         ifd.InfluxDBToken,
   214  			PyroscopeURL:          ifd.PyroscopeURL,
   215  			PyroscopeTrace:        ifd.PyroscopeTrace,
   216  			PyroscopeProfileTypes: ifd.PyroscopeProfileTypes,
   217  			Prometheus:            testnet.Prometheus,
   218  		}
   219  		if node.StartAt == testnet.InitialHeight {
   220  			node.StartAt = 0 // normalize to 0 for initial nodes, since code expects this
   221  		}
   222  		if nodeManifest.Mode != "" {
   223  			node.Mode = Mode(nodeManifest.Mode)
   224  		}
   225  		if node.Mode == ModeLight {
   226  			node.ABCIProtocol = ProtocolBuiltin
   227  		}
   228  		if nodeManifest.Database != "" {
   229  			node.Database = nodeManifest.Database
   230  		}
   231  		if nodeManifest.PrivvalProtocol != "" {
   232  			node.PrivvalProtocol = Protocol(nodeManifest.PrivvalProtocol)
   233  		}
   234  		if nodeManifest.PersistInterval != nil {
   235  			node.PersistInterval = *nodeManifest.PersistInterval
   236  		}
   237  		if node.Prometheus {
   238  			node.PrometheusProxyPort = prometheusProxyPortGen.Next()
   239  		}
   240  		for _, p := range nodeManifest.Perturb {
   241  			node.Perturbations = append(node.Perturbations, Perturbation(p))
   242  		}
   243  		for heightString, misbehavior := range nodeManifest.Misbehaviors {
   244  			height, err := strconv.ParseInt(heightString, 10, 64)
   245  			if err != nil {
   246  				return nil, fmt.Errorf("unable to parse height %s to int64: %w", heightString, err)
   247  			}
   248  			node.Misbehaviors[height] = misbehavior
   249  		}
   250  		testnet.Nodes = append(testnet.Nodes, node)
   251  	}
   252  
   253  	// We do a second pass to set up seeds and persistent peers, which allows graph cycles.
   254  	for _, node := range testnet.Nodes {
   255  		nodeManifest, ok := manifest.Nodes[node.Name]
   256  		if !ok {
   257  			return nil, fmt.Errorf("failed to look up manifest for node %q", node.Name)
   258  		}
   259  		for _, seedName := range nodeManifest.Seeds {
   260  			seed := testnet.LookupNode(seedName)
   261  			if seed == nil {
   262  				return nil, fmt.Errorf("unknown seed %q for node %q", seedName, node.Name)
   263  			}
   264  			node.Seeds = append(node.Seeds, seed)
   265  		}
   266  		for _, peerName := range nodeManifest.PersistentPeers {
   267  			peer := testnet.LookupNode(peerName)
   268  			if peer == nil {
   269  				return nil, fmt.Errorf("unknown persistent peer %q for node %q", peerName, node.Name)
   270  			}
   271  			node.PersistentPeers = append(node.PersistentPeers, peer)
   272  		}
   273  
   274  		// If there are no seeds or persistent peers specified, default to persistent
   275  		// connections to all other nodes.
   276  		if len(node.PersistentPeers) == 0 && len(node.Seeds) == 0 {
   277  			for _, peer := range testnet.Nodes {
   278  				if peer.Name == node.Name {
   279  					continue
   280  				}
   281  				node.PersistentPeers = append(node.PersistentPeers, peer)
   282  			}
   283  		}
   284  	}
   285  
   286  	// Set up genesis validators. If not specified explicitly, use all validator nodes.
   287  	if manifest.Validators != nil {
   288  		for validatorName, power := range *manifest.Validators {
   289  			validator := testnet.LookupNode(validatorName)
   290  			if validator == nil {
   291  				return nil, fmt.Errorf("unknown validator %q", validatorName)
   292  			}
   293  			testnet.Validators[validator] = power
   294  		}
   295  	} else {
   296  		for _, node := range testnet.Nodes {
   297  			if node.Mode == ModeValidator {
   298  				testnet.Validators[node] = 100
   299  			}
   300  		}
   301  	}
   302  
   303  	// Set up validator updates.
   304  	for heightStr, validators := range manifest.ValidatorUpdates {
   305  		height, err := strconv.Atoi(heightStr)
   306  		if err != nil {
   307  			return nil, fmt.Errorf("invalid validator update height %q: %w", height, err)
   308  		}
   309  		valUpdate := map[*Node]int64{}
   310  		for name, power := range validators {
   311  			node := testnet.LookupNode(name)
   312  			if node == nil {
   313  				return nil, fmt.Errorf("unknown validator %q for update at height %v", name, height)
   314  			}
   315  			valUpdate[node] = power
   316  		}
   317  		testnet.ValidatorUpdates[int64(height)] = valUpdate
   318  	}
   319  
   320  	return testnet, testnet.Validate()
   321  }
   322  
   323  // Validate validates a testnet.
   324  func (t Testnet) Validate() error {
   325  	if t.Name == "" {
   326  		return errors.New("network has no name")
   327  	}
   328  	if t.IP == nil {
   329  		return errors.New("network has no IP")
   330  	}
   331  	if t.MaxInboundConnections < 0 {
   332  		return errors.New("MaxInboundConnections must not be negative")
   333  	}
   334  	if t.MaxOutboundConnections < 0 {
   335  		return errors.New("MaxOutboundConnections must not be negative")
   336  	}
   337  	if len(t.Nodes) == 0 {
   338  		return errors.New("network has no nodes")
   339  	}
   340  	for _, node := range t.Nodes {
   341  		if err := node.Validate(t); err != nil {
   342  			return fmt.Errorf("invalid node %q: %w", node.Name, err)
   343  		}
   344  	}
   345  	return nil
   346  }
   347  
   348  // Validate validates a node.
   349  func (n Node) Validate(testnet Testnet) error {
   350  	if n.Name == "" {
   351  		return errors.New("node has no name")
   352  	}
   353  	if n.IP == nil {
   354  		return errors.New("node has no IP address")
   355  	}
   356  	if !testnet.IP.Contains(n.IP) {
   357  		return fmt.Errorf("node IP %v is not in testnet network %v", n.IP, testnet.IP)
   358  	}
   359  	if n.ProxyPort == n.PrometheusProxyPort {
   360  		return fmt.Errorf("node local port %v used also for Prometheus local port", n.ProxyPort)
   361  	}
   362  	if n.ProxyPort > 0 && n.ProxyPort <= 1024 {
   363  		return fmt.Errorf("local port %v must be >1024", n.ProxyPort)
   364  	}
   365  	if n.PrometheusProxyPort > 0 && n.PrometheusProxyPort <= 1024 {
   366  		return fmt.Errorf("local port %v must be >1024", n.PrometheusProxyPort)
   367  	}
   368  	for _, peer := range testnet.Nodes {
   369  		if peer.Name != n.Name && peer.ProxyPort == n.ProxyPort {
   370  			return fmt.Errorf("peer %q also has local port %v", peer.Name, n.ProxyPort)
   371  		}
   372  		if n.PrometheusProxyPort > 0 {
   373  			if peer.Name != n.Name && peer.PrometheusProxyPort == n.PrometheusProxyPort {
   374  				return fmt.Errorf("peer %q also has local port %v", peer.Name, n.PrometheusProxyPort)
   375  			}
   376  		}
   377  	}
   378  	switch n.FastSync {
   379  	case "", "v0", "v1", "v2":
   380  	default:
   381  		return fmt.Errorf("invalid fast sync setting %q", n.FastSync)
   382  
   383  	}
   384  	switch n.Mempool {
   385  	case "", config.MempoolV0, config.MempoolV1, config.MempoolV2:
   386  	default:
   387  		return fmt.Errorf("invalid mempool version %q", n.Mempool)
   388  	}
   389  	switch n.Database {
   390  	case "goleveldb", "cleveldb", "boltdb", "rocksdb", "badgerdb":
   391  	default:
   392  		return fmt.Errorf("invalid database setting %q", n.Database)
   393  	}
   394  	switch n.ABCIProtocol {
   395  	case ProtocolBuiltin, ProtocolUNIX, ProtocolTCP, ProtocolGRPC:
   396  	default:
   397  		return fmt.Errorf("invalid ABCI protocol setting %q", n.ABCIProtocol)
   398  	}
   399  	if n.Mode == ModeLight && n.ABCIProtocol != ProtocolBuiltin {
   400  		return errors.New("light client must use builtin protocol")
   401  	}
   402  	switch n.PrivvalProtocol {
   403  	case ProtocolFile, ProtocolUNIX, ProtocolTCP:
   404  	default:
   405  		return fmt.Errorf("invalid privval protocol setting %q", n.PrivvalProtocol)
   406  	}
   407  
   408  	if n.StartAt > 0 && n.StartAt < n.Testnet.InitialHeight {
   409  		return fmt.Errorf("cannot start at height %v lower than initial height %v",
   410  			n.StartAt, n.Testnet.InitialHeight)
   411  	}
   412  	if n.StateSync && n.StartAt == 0 {
   413  		return errors.New("state synced nodes cannot start at the initial height")
   414  	}
   415  	if n.PersistInterval == 0 && n.RetainBlocks > 0 {
   416  		return errors.New("persist_interval=0 requires retain_blocks=0")
   417  	}
   418  	if n.PersistInterval > 1 && n.RetainBlocks > 0 && n.RetainBlocks < n.PersistInterval {
   419  		return errors.New("persist_interval must be less than or equal to retain_blocks")
   420  	}
   421  	if n.SnapshotInterval > 0 && n.RetainBlocks > 0 && n.RetainBlocks < n.SnapshotInterval {
   422  		return errors.New("snapshot_interval must be less than er equal to retain_blocks")
   423  	}
   424  
   425  	var upgradeFound bool
   426  	for _, perturbation := range n.Perturbations {
   427  		switch perturbation {
   428  		case PerturbationUpgrade:
   429  			if upgradeFound {
   430  				return fmt.Errorf("'upgrade' perturbation can appear at most once per node")
   431  			}
   432  			upgradeFound = true
   433  		case PerturbationDisconnect, PerturbationKill, PerturbationPause, PerturbationRestart:
   434  		default:
   435  			return fmt.Errorf("invalid perturbation %q", perturbation)
   436  		}
   437  	}
   438  
   439  	if (n.PrivvalProtocol != "file" || n.Mode != "validator") && len(n.Misbehaviors) != 0 {
   440  		return errors.New("must be using \"file\" privval protocol to implement misbehaviors")
   441  	}
   442  
   443  	for height, misbehavior := range n.Misbehaviors {
   444  		if height < n.StartAt {
   445  			return fmt.Errorf("misbehavior height %d is below node start height %d",
   446  				height, n.StartAt)
   447  		}
   448  		if height < testnet.InitialHeight {
   449  			return fmt.Errorf("misbehavior height %d is below network initial height %d",
   450  				height, testnet.InitialHeight)
   451  		}
   452  		exists := false
   453  		for possibleBehaviors := range mcs.MisbehaviorList {
   454  			if possibleBehaviors == misbehavior {
   455  				exists = true
   456  			}
   457  		}
   458  		if !exists {
   459  			return fmt.Errorf("misbehavior %s does not exist", misbehavior)
   460  		}
   461  	}
   462  
   463  	return nil
   464  }
   465  
   466  // LookupNode looks up a node by name. For now, simply do a linear search.
   467  func (t Testnet) LookupNode(name string) *Node {
   468  	for _, node := range t.Nodes {
   469  		if node.Name == name {
   470  			return node
   471  		}
   472  	}
   473  	return nil
   474  }
   475  
   476  // ArchiveNodes returns a list of archive nodes that start at the initial height
   477  // and contain the entire blockchain history. They are used e.g. as light client
   478  // RPC servers.
   479  func (t Testnet) ArchiveNodes() []*Node {
   480  	nodes := []*Node{}
   481  	for _, node := range t.Nodes {
   482  		if !node.Stateless() && node.StartAt == 0 && node.RetainBlocks == 0 {
   483  			nodes = append(nodes, node)
   484  		}
   485  	}
   486  	return nodes
   487  }
   488  
   489  // RandomNode returns a random non-seed node.
   490  func (t Testnet) RandomNode() *Node {
   491  	for {
   492  		//nolint:gosec // G404: Use of weak random number generator (math/rand instead of crypto/rand)
   493  		node := t.Nodes[rand.Intn(len(t.Nodes))]
   494  		if node.Mode != ModeSeed {
   495  			return node
   496  		}
   497  	}
   498  }
   499  
   500  // IPv6 returns true if the testnet is an IPv6 network.
   501  func (t Testnet) IPv6() bool {
   502  	return t.IP.IP.To4() == nil
   503  }
   504  
   505  // HasPerturbations returns whether the network has any perturbations.
   506  func (t Testnet) HasPerturbations() bool {
   507  	for _, node := range t.Nodes {
   508  		if len(node.Perturbations) > 0 {
   509  			return true
   510  		}
   511  	}
   512  	return false
   513  }
   514  
   515  // LastMisbehaviorHeight returns the height of the last misbehavior.
   516  func (t Testnet) LastMisbehaviorHeight() int64 {
   517  	lastHeight := int64(0)
   518  	for _, node := range t.Nodes {
   519  		for height := range node.Misbehaviors {
   520  			if height > lastHeight {
   521  				lastHeight = height
   522  			}
   523  		}
   524  	}
   525  	return lastHeight
   526  }
   527  
   528  // Address returns a P2P endpoint address for the node.
   529  func (n Node) AddressP2P(withID bool) string {
   530  	ip := n.IP.String()
   531  	if n.IP.To4() == nil {
   532  		// IPv6 addresses must be wrapped in [] to avoid conflict with : port separator
   533  		ip = fmt.Sprintf("[%v]", ip)
   534  	}
   535  	addr := fmt.Sprintf("%v:26656", ip)
   536  	if withID {
   537  		addr = fmt.Sprintf("%x@%v", n.NodeKey.PubKey().Address().Bytes(), addr)
   538  	}
   539  	return addr
   540  }
   541  
   542  // Address returns an RPC endpoint address for the node.
   543  func (n Node) AddressRPC() string {
   544  	ip := n.IP.String()
   545  	if n.IP.To4() == nil {
   546  		// IPv6 addresses must be wrapped in [] to avoid conflict with : port separator
   547  		ip = fmt.Sprintf("[%v]", ip)
   548  	}
   549  	return fmt.Sprintf("%v:26657", ip)
   550  }
   551  
   552  // Client returns an RPC client for a node.
   553  func (n Node) Client() (*rpchttp.HTTP, error) {
   554  	return rpchttp.New(fmt.Sprintf("http://127.0.0.1:%v", n.ProxyPort), "/websocket")
   555  }
   556  
   557  // Stateless returns true if the node is either a seed node or a light node
   558  func (n Node) Stateless() bool {
   559  	return n.Mode == ModeLight || n.Mode == ModeSeed
   560  }
   561  
   562  // keyGenerator generates pseudorandom Ed25519 keys based on a seed.
   563  type keyGenerator struct {
   564  	random *rand.Rand
   565  }
   566  
   567  func newKeyGenerator(seed int64) *keyGenerator {
   568  	return &keyGenerator{
   569  		random: rand.New(rand.NewSource(seed)), //nolint:gosec
   570  	}
   571  }
   572  
   573  func (g *keyGenerator) Generate(keyType string) crypto.PrivKey {
   574  	seed := make([]byte, ed25519.SeedSize)
   575  
   576  	_, err := io.ReadFull(g.random, seed)
   577  	if err != nil {
   578  		panic(err) // this shouldn't happen
   579  	}
   580  	switch keyType {
   581  	case "secp256k1":
   582  		return secp256k1.GenPrivKeySecp256k1(seed)
   583  	case "", "ed25519":
   584  		return ed25519.GenPrivKeyFromSecret(seed)
   585  	default:
   586  		panic("KeyType not supported") // should not make it this far
   587  	}
   588  }
   589  
   590  // portGenerator generates local Docker proxy ports for each node.
   591  type portGenerator struct {
   592  	nextPort uint32
   593  }
   594  
   595  func newPortGenerator(firstPort uint32) *portGenerator {
   596  	return &portGenerator{nextPort: firstPort}
   597  }
   598  
   599  func (g *portGenerator) Next() uint32 {
   600  	port := g.nextPort
   601  	g.nextPort++
   602  	if g.nextPort == 0 {
   603  		panic("port overflow")
   604  	}
   605  	return port
   606  }
   607  
   608  // ipGenerator generates sequential IP addresses for each node, using a random
   609  // network address.
   610  type ipGenerator struct {
   611  	network *net.IPNet
   612  	nextIP  net.IP
   613  }
   614  
   615  func newIPGenerator(network *net.IPNet) *ipGenerator {
   616  	nextIP := make([]byte, len(network.IP))
   617  	copy(nextIP, network.IP)
   618  	gen := &ipGenerator{network: network, nextIP: nextIP}
   619  	// Skip network and gateway addresses
   620  	gen.Next()
   621  	gen.Next()
   622  	return gen
   623  }
   624  
   625  func (g *ipGenerator) Network() *net.IPNet {
   626  	n := &net.IPNet{
   627  		IP:   make([]byte, len(g.network.IP)),
   628  		Mask: make([]byte, len(g.network.Mask)),
   629  	}
   630  	copy(n.IP, g.network.IP)
   631  	copy(n.Mask, g.network.Mask)
   632  	return n
   633  }
   634  
   635  func (g *ipGenerator) Next() net.IP {
   636  	ip := make([]byte, len(g.nextIP))
   637  	copy(ip, g.nextIP)
   638  	for i := len(g.nextIP) - 1; i >= 0; i-- {
   639  		g.nextIP[i]++
   640  		if g.nextIP[i] != 0 {
   641  			break
   642  		}
   643  	}
   644  	return ip
   645  }