//nolint: gosec package e2e import ( "errors" "fmt" "io" "math/rand" "net" "path/filepath" "sort" "strconv" "strings" "time" "github.com/tendermint/tendermint/crypto" "github.com/tendermint/tendermint/crypto/ed25519" "github.com/tendermint/tendermint/crypto/secp256k1" rpchttp "github.com/tendermint/tendermint/rpc/client/http" "github.com/tendermint/tendermint/types" ) const ( randomSeed int64 = 2308084734268 proxyPortFirst uint32 = 5701 networkIPv4 = "10.186.73.0/24" networkIPv6 = "fd80:b10c::/48" ) type Mode string type Protocol string type Perturbation string const ( ModeValidator Mode = "validator" ModeFull Mode = "full" ModeLight Mode = "light" ModeSeed Mode = "seed" ProtocolBuiltin Protocol = "builtin" ProtocolFile Protocol = "file" ProtocolGRPC Protocol = "grpc" ProtocolTCP Protocol = "tcp" ProtocolUNIX Protocol = "unix" PerturbationDisconnect Perturbation = "disconnect" PerturbationKill Perturbation = "kill" PerturbationPause Perturbation = "pause" PerturbationRestart Perturbation = "restart" EvidenceAgeHeight int64 = 7 EvidenceAgeTime time.Duration = 500 * time.Millisecond StateSyncP2P = "p2p" StateSyncRPC = "rpc" StateSyncDisabled = "" ) // Testnet represents a single testnet. type Testnet struct { Name string File string Dir string IP *net.IPNet InitialHeight int64 InitialState map[string]string Validators map[*Node]int64 ValidatorUpdates map[int64]map[*Node]int64 Nodes []*Node KeyType string Evidence int LogLevel string TxSize int ABCIProtocol string } // Node represents a Tendermint node in a testnet. type Node struct { Name string Testnet *Testnet Mode Mode PrivvalKey crypto.PrivKey NodeKey crypto.PrivKey IP net.IP ProxyPort uint32 StartAt int64 Mempool string StateSync string Database string ABCIProtocol Protocol PrivvalProtocol Protocol PersistInterval uint64 SnapshotInterval uint64 RetainBlocks uint64 Seeds []*Node PersistentPeers []*Node Perturbations []Perturbation LogLevel string QueueType string HasStarted bool } // LoadTestnet loads a testnet from a manifest file, using the filename to // determine the testnet name and directory (from the basename of the file). // The testnet generation must be deterministic, since it is generated // separately by the runner and the test cases. For this reason, testnets use a // random seed to generate e.g. keys. func LoadTestnet(file string) (*Testnet, error) { manifest, err := LoadManifest(file) if err != nil { return nil, err } dir := strings.TrimSuffix(file, filepath.Ext(file)) // Set up resource generators. These must be deterministic. netAddress := networkIPv4 if manifest.IPv6 { netAddress = networkIPv6 } _, ipNet, err := net.ParseCIDR(netAddress) if err != nil { return nil, fmt.Errorf("invalid IP network address %q: %w", netAddress, err) } ipGen := newIPGenerator(ipNet) keyGen := newKeyGenerator(randomSeed) proxyPortGen := newPortGenerator(proxyPortFirst) testnet := &Testnet{ Name: filepath.Base(dir), File: file, Dir: dir, IP: ipGen.Network(), InitialHeight: 1, InitialState: manifest.InitialState, Validators: map[*Node]int64{}, ValidatorUpdates: map[int64]map[*Node]int64{}, Nodes: []*Node{}, Evidence: manifest.Evidence, KeyType: "ed25519", LogLevel: manifest.LogLevel, TxSize: manifest.TxSize, ABCIProtocol: manifest.ABCIProtocol, } if len(manifest.KeyType) != 0 { testnet.KeyType = manifest.KeyType } if testnet.TxSize <= 0 { testnet.TxSize = 1024 } if manifest.InitialHeight > 0 { testnet.InitialHeight = manifest.InitialHeight } if testnet.ABCIProtocol == "" { testnet.ABCIProtocol = string(ProtocolBuiltin) } // Set up nodes, in alphabetical order (IPs and ports get same order). nodeNames := []string{} for name := range manifest.Nodes { nodeNames = append(nodeNames, name) } sort.Strings(nodeNames) for _, name := range nodeNames { nodeManifest := manifest.Nodes[name] node := &Node{ Name: name, Testnet: testnet, PrivvalKey: keyGen.Generate(manifest.KeyType), NodeKey: keyGen.Generate("ed25519"), IP: ipGen.Next(), ProxyPort: proxyPortGen.Next(), Mode: ModeValidator, Database: "goleveldb", ABCIProtocol: Protocol(testnet.ABCIProtocol), PrivvalProtocol: ProtocolFile, StartAt: nodeManifest.StartAt, Mempool: nodeManifest.Mempool, StateSync: nodeManifest.StateSync, PersistInterval: 1, SnapshotInterval: nodeManifest.SnapshotInterval, RetainBlocks: nodeManifest.RetainBlocks, Perturbations: []Perturbation{}, LogLevel: manifest.LogLevel, QueueType: manifest.QueueType, } if node.StartAt == testnet.InitialHeight { node.StartAt = 0 // normalize to 0 for initial nodes, since code expects this } if nodeManifest.Mode != "" { node.Mode = Mode(nodeManifest.Mode) } if node.Mode == ModeLight { node.ABCIProtocol = ProtocolBuiltin } if nodeManifest.Database != "" { node.Database = nodeManifest.Database } if nodeManifest.PrivvalProtocol != "" { node.PrivvalProtocol = Protocol(nodeManifest.PrivvalProtocol) } if nodeManifest.PersistInterval != nil { node.PersistInterval = *nodeManifest.PersistInterval } for _, p := range nodeManifest.Perturb { node.Perturbations = append(node.Perturbations, Perturbation(p)) } if nodeManifest.LogLevel != "" { node.LogLevel = nodeManifest.LogLevel } testnet.Nodes = append(testnet.Nodes, node) } // We do a second pass to set up seeds and persistent peers, which allows graph cycles. for _, node := range testnet.Nodes { nodeManifest, ok := manifest.Nodes[node.Name] if !ok { return nil, fmt.Errorf("failed to look up manifest for node %q", node.Name) } for _, seedName := range nodeManifest.Seeds { seed := testnet.LookupNode(seedName) if seed == nil { return nil, fmt.Errorf("unknown seed %q for node %q", seedName, node.Name) } node.Seeds = append(node.Seeds, seed) } for _, peerName := range nodeManifest.PersistentPeers { peer := testnet.LookupNode(peerName) if peer == nil { return nil, fmt.Errorf("unknown persistent peer %q for node %q", peerName, node.Name) } if peer.Mode == ModeLight { return nil, fmt.Errorf("can not have a light client as a persistent peer (for %q)", node.Name) } node.PersistentPeers = append(node.PersistentPeers, peer) } // If there are no seeds or persistent peers specified, default to persistent // connections to all other full nodes. if len(node.PersistentPeers) == 0 && len(node.Seeds) == 0 { for _, peer := range testnet.Nodes { if peer.Name == node.Name { continue } if peer.Mode == ModeLight { continue } node.PersistentPeers = append(node.PersistentPeers, peer) } } } // Set up genesis validators. If not specified explicitly, use all validator nodes. if manifest.Validators != nil { for validatorName, power := range *manifest.Validators { validator := testnet.LookupNode(validatorName) if validator == nil { return nil, fmt.Errorf("unknown validator %q", validatorName) } testnet.Validators[validator] = power } } else { for _, node := range testnet.Nodes { if node.Mode == ModeValidator { testnet.Validators[node] = 100 } } } // Set up validator updates. for heightStr, validators := range manifest.ValidatorUpdates { height, err := strconv.Atoi(heightStr) if err != nil { return nil, fmt.Errorf("invalid validator update height %q: %w", height, err) } valUpdate := map[*Node]int64{} for name, power := range validators { node := testnet.LookupNode(name) if node == nil { return nil, fmt.Errorf("unknown validator %q for update at height %v", name, height) } valUpdate[node] = power } testnet.ValidatorUpdates[int64(height)] = valUpdate } return testnet, testnet.Validate() } // Validate validates a testnet. func (t Testnet) Validate() error { if t.Name == "" { return errors.New("network has no name") } if t.IP == nil { return errors.New("network has no IP") } if len(t.Nodes) == 0 { return errors.New("network has no nodes") } switch t.KeyType { case "", types.ABCIPubKeyTypeEd25519, types.ABCIPubKeyTypeSecp256k1: default: return errors.New("unsupported KeyType") } for _, node := range t.Nodes { if err := node.Validate(t); err != nil { return fmt.Errorf("invalid node %q: %w", node.Name, err) } } return nil } // Validate validates a node. func (n Node) Validate(testnet Testnet) error { if n.Name == "" { return errors.New("node has no name") } if n.IP == nil { return errors.New("node has no IP address") } if !testnet.IP.Contains(n.IP) { return fmt.Errorf("node IP %v is not in testnet network %v", n.IP, testnet.IP) } if n.ProxyPort > 0 { if n.ProxyPort <= 1024 { return fmt.Errorf("local port %v must be >1024", n.ProxyPort) } for _, peer := range testnet.Nodes { if peer.Name != n.Name && peer.ProxyPort == n.ProxyPort { return fmt.Errorf("peer %q also has local port %v", peer.Name, n.ProxyPort) } } } switch n.StateSync { case StateSyncDisabled, StateSyncP2P, StateSyncRPC: default: return fmt.Errorf("invalid state sync setting %q", n.StateSync) } switch n.Mempool { case "", "v0", "v1": default: return fmt.Errorf("invalid mempool version %q", n.Mempool) } switch n.QueueType { case "", "priority", "fifo": default: return fmt.Errorf("unsupported p2p queue type: %s", n.QueueType) } switch n.Database { case "goleveldb", "cleveldb", "boltdb", "rocksdb", "badgerdb": default: return fmt.Errorf("invalid database setting %q", n.Database) } switch n.ABCIProtocol { case ProtocolBuiltin, ProtocolUNIX, ProtocolTCP, ProtocolGRPC: default: return fmt.Errorf("invalid ABCI protocol setting %q", n.ABCIProtocol) } if n.Mode == ModeLight && n.ABCIProtocol != ProtocolBuiltin { return errors.New("light client must use builtin protocol") } switch n.PrivvalProtocol { case ProtocolFile, ProtocolTCP, ProtocolGRPC, ProtocolUNIX: default: return fmt.Errorf("invalid privval protocol setting %q", n.PrivvalProtocol) } if n.StartAt > 0 && n.StartAt < n.Testnet.InitialHeight { return fmt.Errorf("cannot start at height %v lower than initial height %v", n.StartAt, n.Testnet.InitialHeight) } if n.StateSync != StateSyncDisabled && n.StartAt == 0 { return errors.New("state synced nodes cannot start at the initial height") } if n.RetainBlocks != 0 && n.RetainBlocks < uint64(EvidenceAgeHeight) { return fmt.Errorf("retain_blocks must be greater or equal to max evidence age (%d)", EvidenceAgeHeight) } if n.PersistInterval == 0 && n.RetainBlocks > 0 { return errors.New("persist_interval=0 requires retain_blocks=0") } if n.PersistInterval > 1 && n.RetainBlocks > 0 && n.RetainBlocks < n.PersistInterval { return errors.New("persist_interval must be less than or equal to retain_blocks") } if n.SnapshotInterval > 0 && n.RetainBlocks > 0 && n.RetainBlocks < n.SnapshotInterval { return errors.New("snapshot_interval must be less than er equal to retain_blocks") } for _, perturbation := range n.Perturbations { switch perturbation { case PerturbationDisconnect, PerturbationKill, PerturbationPause, PerturbationRestart: default: return fmt.Errorf("invalid perturbation %q", perturbation) } } return nil } // LookupNode looks up a node by name. For now, simply do a linear search. func (t Testnet) LookupNode(name string) *Node { for _, node := range t.Nodes { if node.Name == name { return node } } return nil } // ArchiveNodes returns a list of archive nodes that start at the initial height // and contain the entire blockchain history. They are used e.g. as light client // RPC servers. func (t Testnet) ArchiveNodes() []*Node { nodes := []*Node{} for _, node := range t.Nodes { if !node.Stateless() && node.StartAt == 0 && node.RetainBlocks == 0 { nodes = append(nodes, node) } } return nodes } // IPv6 returns true if the testnet is an IPv6 network. func (t Testnet) IPv6() bool { return t.IP.IP.To4() == nil } // HasPerturbations returns whether the network has any perturbations. func (t Testnet) HasPerturbations() bool { for _, node := range t.Nodes { if len(node.Perturbations) > 0 { return true } } return false } // Address returns a P2P endpoint address for the node. func (n Node) AddressP2P(withID bool) string { ip := n.IP.String() if n.IP.To4() == nil { // IPv6 addresses must be wrapped in [] to avoid conflict with : port separator ip = fmt.Sprintf("[%v]", ip) } addr := fmt.Sprintf("%v:26656", ip) if withID { addr = fmt.Sprintf("%x@%v", n.NodeKey.PubKey().Address().Bytes(), addr) } return addr } // Address returns an RPC endpoint address for the node. func (n Node) AddressRPC() string { ip := n.IP.String() if n.IP.To4() == nil { // IPv6 addresses must be wrapped in [] to avoid conflict with : port separator ip = fmt.Sprintf("[%v]", ip) } return fmt.Sprintf("%v:26657", ip) } // Client returns an RPC client for a node. func (n Node) Client() (*rpchttp.HTTP, error) { return rpchttp.New(fmt.Sprintf("http://127.0.0.1:%v", n.ProxyPort)) } // Stateless returns true if the node is either a seed node or a light node func (n Node) Stateless() bool { return n.Mode == ModeLight || n.Mode == ModeSeed } // keyGenerator generates pseudorandom Ed25519 keys based on a seed. type keyGenerator struct { random *rand.Rand } func newKeyGenerator(seed int64) *keyGenerator { return &keyGenerator{ random: rand.New(rand.NewSource(seed)), } } func (g *keyGenerator) Generate(keyType string) crypto.PrivKey { seed := make([]byte, ed25519.SeedSize) _, err := io.ReadFull(g.random, seed) if err != nil { panic(err) // this shouldn't happen } switch keyType { case "secp256k1": return secp256k1.GenPrivKeySecp256k1(seed) case "", "ed25519": return ed25519.GenPrivKeyFromSecret(seed) default: panic("KeyType not supported") // should not make it this far } } // portGenerator generates local Docker proxy ports for each node. type portGenerator struct { nextPort uint32 } func newPortGenerator(firstPort uint32) *portGenerator { return &portGenerator{nextPort: firstPort} } func (g *portGenerator) Next() uint32 { port := g.nextPort g.nextPort++ if g.nextPort == 0 { panic("port overflow") } return port } // ipGenerator generates sequential IP addresses for each node, using a random // network address. type ipGenerator struct { network *net.IPNet nextIP net.IP } func newIPGenerator(network *net.IPNet) *ipGenerator { nextIP := make([]byte, len(network.IP)) copy(nextIP, network.IP) gen := &ipGenerator{network: network, nextIP: nextIP} // Skip network and gateway addresses gen.Next() gen.Next() return gen } func (g *ipGenerator) Network() *net.IPNet { n := &net.IPNet{ IP: make([]byte, len(g.network.IP)), Mask: make([]byte, len(g.network.Mask)), } copy(n.IP, g.network.IP) copy(n.Mask, g.network.Mask) return n } func (g *ipGenerator) Next() net.IP { ip := make([]byte, len(g.nextIP)) copy(ip, g.nextIP) for i := len(g.nextIP) - 1; i >= 0; i-- { g.nextIP[i]++ if g.nextIP[i] != 0 { break } } return ip }