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 }