github.com/consideritdone/landslidecore@v0.0.0-20230718131026-a8b21c5cf8a7/test/e2e/pkg/testnet.go (about)

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