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