You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

470 lines
13 KiB

//nolint: gosec
package e2e
import (
"errors"
"fmt"
"io"
"math/rand"
"net"
"path/filepath"
"sort"
"strconv"
"strings"
"github.com/tendermint/tendermint/crypto"
"github.com/tendermint/tendermint/crypto/ed25519"
rpchttp "github.com/tendermint/tendermint/rpc/client/http"
)
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"
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"
)
// 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
}
// Node represents a Tendermint node in a testnet.
type Node struct {
Name string
Testnet *Testnet
Mode Mode
Key crypto.PrivKey
IP net.IP
ProxyPort uint32
StartAt int64
FastSync string
StateSync bool
Database string
ABCIProtocol Protocol
PrivvalProtocol Protocol
PersistInterval uint64
SnapshotInterval uint64
RetainBlocks uint64
Seeds []*Node
PersistentPeers []*Node
Perturbations []Perturbation
}
// 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{},
}
if manifest.InitialHeight > 0 {
testnet.InitialHeight = manifest.InitialHeight
}
// 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,
Key: keyGen.Generate(),
IP: ipGen.Next(),
ProxyPort: proxyPortGen.Next(),
Mode: ModeValidator,
Database: "goleveldb",
ABCIProtocol: ProtocolUNIX,
PrivvalProtocol: ProtocolFile,
StartAt: nodeManifest.StartAt,
FastSync: nodeManifest.FastSync,
StateSync: nodeManifest.StateSync,
PersistInterval: 1,
SnapshotInterval: nodeManifest.SnapshotInterval,
RetainBlocks: nodeManifest.RetainBlocks,
Perturbations: []Perturbation{},
}
if nodeManifest.Mode != "" {
node.Mode = Mode(nodeManifest.Mode)
}
if nodeManifest.Database != "" {
node.Database = nodeManifest.Database
}
if nodeManifest.ABCIProtocol != "" {
node.ABCIProtocol = Protocol(nodeManifest.ABCIProtocol)
}
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))
}
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)
}
node.PersistentPeers = append(node.PersistentPeers, peer)
}
// If there are no seeds or persistent peers specified, default to persistent
// connections to all other nodes.
if len(node.PersistentPeers) == 0 && len(node.Seeds) == 0 {
for _, peer := range testnet.Nodes {
if peer.Name == node.Name {
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")
}
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.FastSync {
case "", "v0", "v1", "v2":
default:
return fmt.Errorf("invalid fast sync setting %q", n.FastSync)
}
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)
}
switch n.PrivvalProtocol {
case ProtocolFile, ProtocolUNIX, ProtocolTCP:
default:
return fmt.Errorf("invalid privval protocol setting %q", n.PrivvalProtocol)
}
if n.StateSync && n.StartAt == 0 {
return errors.New("state synced nodes cannot start at the initial height")
}
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.Mode != ModeSeed && node.StartAt == 0 && node.RetainBlocks == 0 {
nodes = append(nodes, node)
}
}
return nodes
}
// RandomNode returns a random non-seed node.
func (t Testnet) RandomNode() *Node {
for {
node := t.Nodes[rand.Intn(len(t.Nodes))]
if node.Mode != ModeSeed {
return node
}
}
}
// IPv6 returns true if the testnet is an IPv6 network.
func (t Testnet) IPv6() bool {
return t.IP.IP.To4() == nil
}
// 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.Key.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), "/websocket")
}
// 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() crypto.PrivKey {
seed := make([]byte, ed25519.SeedSize)
_, err := io.ReadFull(g.random, seed)
if err != nil {
panic(err) // this shouldn't happen
}
return ed25519.GenPrivKeyFromSecret(seed)
}
// 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
}