github.com/badrootd/nibiru-cometbft@v0.37.5-0.20240307173500-2a75559eee9b/test/e2e/generator/generate.go (about) 1 package main 2 3 import ( 4 "errors" 5 "fmt" 6 "math/rand" 7 "sort" 8 "strconv" 9 "strings" 10 "time" 11 12 "github.com/Masterminds/semver/v3" 13 e2e "github.com/badrootd/nibiru-cometbft/test/e2e/pkg" 14 "github.com/badrootd/nibiru-cometbft/version" 15 "github.com/go-git/go-git/v5" 16 "github.com/go-git/go-git/v5/plumbing/object" 17 ) 18 19 var ( 20 // testnetCombinations defines global testnet options, where we generate a 21 // separate testnet for each combination (Cartesian product) of options. 22 testnetCombinations = map[string][]interface{}{ 23 "topology": {"single", "quad", "large"}, 24 "initialHeight": {0, 1000}, 25 "initialState": { 26 map[string]string{}, 27 map[string]string{"initial01": "a", "initial02": "b", "initial03": "c"}, 28 }, 29 "validators": {"genesis", "initchain"}, 30 } 31 nodeVersions = weightedChoice{ 32 "": 2, 33 } 34 35 // The following specify randomly chosen values for testnet nodes. 36 nodeDatabases = uniformChoice{"goleveldb", "cleveldb", "rocksdb", "boltdb", "badgerdb"} 37 ipv6 = uniformChoice{false, true} 38 // FIXME: grpc disabled due to https://github.com/tendermint/tendermint/issues/5439 39 nodeABCIProtocols = uniformChoice{"unix", "tcp", "builtin"} // "grpc" 40 nodePrivvalProtocols = uniformChoice{"file", "unix", "tcp"} 41 nodeBlockSyncs = uniformChoice{"v0"} // "v2" 42 nodeStateSyncs = uniformChoice{false, true} 43 nodeMempools = uniformChoice{"v0", "v1"} 44 nodePersistIntervals = uniformChoice{0, 1, 5} 45 nodeSnapshotIntervals = uniformChoice{0, 3} 46 nodeRetainBlocks = uniformChoice{ 47 0, 48 2 * int(e2e.EvidenceAgeHeight), 49 4 * int(e2e.EvidenceAgeHeight), 50 } 51 evidence = uniformChoice{0, 1, 10} 52 abciDelays = uniformChoice{"none", "small", "large"} 53 nodePerturbations = probSetChoice{ 54 "disconnect": 0.1, 55 "pause": 0.1, 56 "kill": 0.1, 57 "restart": 0.1, 58 "upgrade": 0.3, 59 } 60 lightNodePerturbations = probSetChoice{ 61 "upgrade": 0.3, 62 } 63 ) 64 65 type generateConfig struct { 66 randSource *rand.Rand 67 outputDir string 68 multiVersion string 69 prometheus bool 70 } 71 72 // Generate generates random testnets using the given RNG. 73 func Generate(cfg *generateConfig) ([]e2e.Manifest, error) { 74 upgradeVersion := "" 75 76 if cfg.multiVersion != "" { 77 var err error 78 nodeVersions, upgradeVersion, err = parseWeightedVersions(cfg.multiVersion) 79 if err != nil { 80 return nil, err 81 } 82 if _, ok := nodeVersions["local"]; ok { 83 nodeVersions[""] = nodeVersions["local"] 84 delete(nodeVersions, "local") 85 if upgradeVersion == "local" { 86 upgradeVersion = "" 87 } 88 } 89 if _, ok := nodeVersions["latest"]; ok { 90 latestVersion, err := gitRepoLatestReleaseVersion(cfg.outputDir) 91 if err != nil { 92 return nil, err 93 } 94 nodeVersions[latestVersion] = nodeVersions["latest"] 95 delete(nodeVersions, "latest") 96 if upgradeVersion == "latest" { 97 upgradeVersion = latestVersion 98 } 99 } 100 } 101 fmt.Println("Generating testnet with weighted versions:") 102 for ver, wt := range nodeVersions { 103 if ver == "" { 104 fmt.Printf("- local: %d\n", wt) 105 } else { 106 fmt.Printf("- %s: %d\n", ver, wt) 107 } 108 } 109 manifests := []e2e.Manifest{} 110 for _, opt := range combinations(testnetCombinations) { 111 manifest, err := generateTestnet(cfg.randSource, opt, upgradeVersion, cfg.prometheus) 112 if err != nil { 113 return nil, err 114 } 115 manifests = append(manifests, manifest) 116 } 117 return manifests, nil 118 } 119 120 // generateTestnet generates a single testnet with the given options. 121 func generateTestnet(r *rand.Rand, opt map[string]interface{}, upgradeVersion string, prometheus bool) (e2e.Manifest, error) { 122 manifest := e2e.Manifest{ 123 IPv6: ipv6.Choose(r).(bool), 124 ABCIProtocol: nodeABCIProtocols.Choose(r).(string), 125 InitialHeight: int64(opt["initialHeight"].(int)), 126 InitialState: opt["initialState"].(map[string]string), 127 Validators: &map[string]int64{}, 128 ValidatorUpdates: map[string]map[string]int64{}, 129 Evidence: evidence.Choose(r).(int), 130 Nodes: map[string]*e2e.ManifestNode{}, 131 UpgradeVersion: upgradeVersion, 132 Prometheus: prometheus, 133 } 134 135 switch abciDelays.Choose(r).(string) { 136 case "none": 137 case "small": 138 manifest.PrepareProposalDelay = 100 * time.Millisecond 139 manifest.ProcessProposalDelay = 100 * time.Millisecond 140 case "large": 141 manifest.PrepareProposalDelay = 200 * time.Millisecond 142 manifest.ProcessProposalDelay = 200 * time.Millisecond 143 manifest.CheckTxDelay = 20 * time.Millisecond 144 } 145 146 var numSeeds, numValidators, numFulls, numLightClients int 147 switch opt["topology"].(string) { 148 case "single": 149 numValidators = 1 150 case "quad": 151 numValidators = 4 152 case "large": 153 // FIXME Networks are kept small since large ones use too much CPU. 154 numSeeds = r.Intn(2) 155 numLightClients = r.Intn(3) 156 numValidators = 4 + r.Intn(4) 157 numFulls = r.Intn(4) 158 default: 159 return manifest, fmt.Errorf("unknown topology %q", opt["topology"]) 160 } 161 162 // First we generate seed nodes, starting at the initial height. 163 for i := 1; i <= numSeeds; i++ { 164 manifest.Nodes[fmt.Sprintf("seed%02d", i)] = generateNode( 165 r, e2e.ModeSeed, 0, manifest.InitialHeight, false) 166 } 167 168 // Next, we generate validators. We make sure a BFT quorum of validators start 169 // at the initial height, and that we have two archive nodes. We also set up 170 // the initial validator set, and validator set updates for delayed nodes. 171 nextStartAt := manifest.InitialHeight + 5 172 quorum := numValidators*2/3 + 1 173 for i := 1; i <= numValidators; i++ { 174 startAt := int64(0) 175 if i > quorum { 176 startAt = nextStartAt 177 nextStartAt += 5 178 } 179 name := fmt.Sprintf("validator%02d", i) 180 manifest.Nodes[name] = generateNode( 181 r, e2e.ModeValidator, startAt, manifest.InitialHeight, i <= 2) 182 183 if startAt == 0 { 184 (*manifest.Validators)[name] = int64(30 + r.Intn(71)) 185 } else { 186 manifest.ValidatorUpdates[fmt.Sprint(startAt+5)] = map[string]int64{ 187 name: int64(30 + r.Intn(71)), 188 } 189 } 190 } 191 192 // Move validators to InitChain if specified. 193 switch opt["validators"].(string) { 194 case "genesis": 195 case "initchain": 196 manifest.ValidatorUpdates["0"] = *manifest.Validators 197 manifest.Validators = &map[string]int64{} 198 default: 199 return manifest, fmt.Errorf("invalid validators option %q", opt["validators"]) 200 } 201 202 // Finally, we generate random full nodes. 203 for i := 1; i <= numFulls; i++ { 204 startAt := int64(0) 205 if r.Float64() >= 0.5 { 206 startAt = nextStartAt 207 nextStartAt += 5 208 } 209 manifest.Nodes[fmt.Sprintf("full%02d", i)] = generateNode( 210 r, e2e.ModeFull, startAt, manifest.InitialHeight, false) 211 } 212 213 // We now set up peer discovery for nodes. Seed nodes are fully meshed with 214 // each other, while non-seed nodes either use a set of random seeds or a 215 // set of random peers that start before themselves. 216 var seedNames, peerNames, lightProviders []string 217 for name, node := range manifest.Nodes { 218 if node.Mode == string(e2e.ModeSeed) { 219 seedNames = append(seedNames, name) 220 } else { 221 // if the full node or validator is an ideal candidate, it is added as a light provider. 222 // There are at least two archive nodes so there should be at least two ideal candidates 223 if (node.StartAt == 0 || node.StartAt == manifest.InitialHeight) && node.RetainBlocks == 0 { 224 lightProviders = append(lightProviders, name) 225 } 226 peerNames = append(peerNames, name) 227 } 228 } 229 230 for _, name := range seedNames { 231 for _, otherName := range seedNames { 232 if name != otherName { 233 manifest.Nodes[name].Seeds = append(manifest.Nodes[name].Seeds, otherName) 234 } 235 } 236 } 237 238 sort.Slice(peerNames, func(i, j int) bool { 239 iName, jName := peerNames[i], peerNames[j] 240 switch { 241 case manifest.Nodes[iName].StartAt < manifest.Nodes[jName].StartAt: 242 return true 243 case manifest.Nodes[iName].StartAt > manifest.Nodes[jName].StartAt: 244 return false 245 default: 246 return strings.Compare(iName, jName) == -1 247 } 248 }) 249 for i, name := range peerNames { 250 if len(seedNames) > 0 && (i == 0 || r.Float64() >= 0.5) { 251 manifest.Nodes[name].Seeds = uniformSetChoice(seedNames).Choose(r) 252 } else if i > 0 { 253 manifest.Nodes[name].PersistentPeers = uniformSetChoice(peerNames[:i]).Choose(r) 254 } 255 } 256 257 // lastly, set up the light clients 258 for i := 1; i <= numLightClients; i++ { 259 startAt := manifest.InitialHeight + 5 260 manifest.Nodes[fmt.Sprintf("light%02d", i)] = generateLightNode( 261 r, startAt+(5*int64(i)), lightProviders, 262 ) 263 } 264 265 return manifest, nil 266 } 267 268 // generateNode randomly generates a node, with some constraints to avoid 269 // generating invalid configurations. We do not set Seeds or PersistentPeers 270 // here, since we need to know the overall network topology and startup 271 // sequencing. 272 func generateNode( 273 r *rand.Rand, mode e2e.Mode, startAt int64, initialHeight int64, forceArchive bool, 274 ) *e2e.ManifestNode { 275 node := e2e.ManifestNode{ 276 Version: nodeVersions.Choose(r).(string), 277 Mode: string(mode), 278 StartAt: startAt, 279 Database: nodeDatabases.Choose(r).(string), 280 PrivvalProtocol: nodePrivvalProtocols.Choose(r).(string), 281 BlockSync: nodeBlockSyncs.Choose(r).(string), 282 Mempool: nodeMempools.Choose(r).(string), 283 StateSync: nodeStateSyncs.Choose(r).(bool) && startAt > 0, 284 PersistInterval: ptrUint64(uint64(nodePersistIntervals.Choose(r).(int))), 285 SnapshotInterval: uint64(nodeSnapshotIntervals.Choose(r).(int)), 286 RetainBlocks: uint64(nodeRetainBlocks.Choose(r).(int)), 287 Perturb: nodePerturbations.Choose(r), 288 } 289 290 // If this node is forced to be an archive node, retain all blocks and 291 // enable state sync snapshotting. 292 if forceArchive { 293 node.RetainBlocks = 0 294 node.SnapshotInterval = 3 295 } 296 297 // If a node which does not persist state also does not retain blocks, randomly 298 // choose to either persist state or retain all blocks. 299 if node.PersistInterval != nil && *node.PersistInterval == 0 && node.RetainBlocks > 0 { 300 if r.Float64() > 0.5 { 301 node.RetainBlocks = 0 302 } else { 303 node.PersistInterval = ptrUint64(node.RetainBlocks) 304 } 305 } 306 307 // If either PersistInterval or SnapshotInterval are greater than RetainBlocks, 308 // expand the block retention time. 309 if node.RetainBlocks > 0 { 310 if node.PersistInterval != nil && node.RetainBlocks < *node.PersistInterval { 311 node.RetainBlocks = *node.PersistInterval 312 } 313 if node.RetainBlocks < node.SnapshotInterval { 314 node.RetainBlocks = node.SnapshotInterval 315 } 316 } 317 318 return &node 319 } 320 321 func generateLightNode(r *rand.Rand, startAt int64, providers []string) *e2e.ManifestNode { 322 return &e2e.ManifestNode{ 323 Mode: string(e2e.ModeLight), 324 Version: nodeVersions.Choose(r).(string), 325 StartAt: startAt, 326 Database: nodeDatabases.Choose(r).(string), 327 PersistInterval: ptrUint64(0), 328 PersistentPeers: providers, 329 Perturb: lightNodePerturbations.Choose(r), 330 } 331 } 332 333 func ptrUint64(i uint64) *uint64 { 334 return &i 335 } 336 337 // Parses strings like "v0.34.21:1,v0.34.22:2" to represent two versions 338 // ("v0.34.21" and "v0.34.22") with weights of 1 and 2 respectively. 339 // Versions may be specified as cometbft/e2e-node:v0.34.27-alpha.1:1 or 340 // ghcr.io/informalsystems/tendermint:v0.34.26:1. 341 // If only the tag and weight are specified, cometbft/e2e-node is assumed. 342 // Also returns the last version in the list, which will be used for updates. 343 func parseWeightedVersions(s string) (weightedChoice, string, error) { 344 wc := make(weightedChoice) 345 lv := "" 346 wvs := strings.Split(strings.TrimSpace(s), ",") 347 for _, wv := range wvs { 348 parts := strings.Split(strings.TrimSpace(wv), ":") 349 var ver string 350 if len(parts) == 2 { 351 ver = strings.TrimSpace(strings.Join([]string{"cometbft/e2e-node", parts[0]}, ":")) 352 } else if len(parts) == 3 { 353 ver = strings.TrimSpace(strings.Join([]string{parts[0], parts[1]}, ":")) 354 } else { 355 return nil, "", fmt.Errorf("unexpected weight:version combination: %s", wv) 356 } 357 358 wt, err := strconv.Atoi(strings.TrimSpace(parts[len(parts)-1])) 359 if err != nil { 360 return nil, "", fmt.Errorf("unexpected weight \"%s\": %w", parts[1], err) 361 } 362 363 if wt < 1 { 364 return nil, "", errors.New("version weights must be >= 1") 365 } 366 wc[ver] = uint(wt) 367 lv = ver 368 } 369 return wc, lv, nil 370 } 371 372 // Extracts the latest release version from the given Git repository. Uses the 373 // current version of CometBFT to establish the "major" version 374 // currently in use. 375 func gitRepoLatestReleaseVersion(gitRepoDir string) (string, error) { 376 opts := &git.PlainOpenOptions{ 377 DetectDotGit: true, 378 } 379 r, err := git.PlainOpenWithOptions(gitRepoDir, opts) 380 if err != nil { 381 return "", err 382 } 383 tags := make([]string, 0) 384 tagObjs, err := r.TagObjects() 385 if err != nil { 386 return "", err 387 } 388 err = tagObjs.ForEach(func(tagObj *object.Tag) error { 389 tags = append(tags, tagObj.Name) 390 return nil 391 }) 392 if err != nil { 393 return "", err 394 } 395 return findLatestReleaseTag(version.TMCoreSemVer, tags) 396 } 397 398 func findLatestReleaseTag(baseVer string, tags []string) (string, error) { 399 baseSemVer, err := semver.NewVersion(strings.Split(baseVer, "-")[0]) 400 if err != nil { 401 return "", fmt.Errorf("failed to parse base version \"%s\": %w", baseVer, err) 402 } 403 compVer := fmt.Sprintf("%d.%d", baseSemVer.Major(), baseSemVer.Minor()) 404 // Build our version comparison string 405 // See https://github.com/Masterminds/semver#caret-range-comparisons-major for details 406 compStr := "^ " + compVer 407 verCon, err := semver.NewConstraint(compStr) 408 if err != nil { 409 return "", err 410 } 411 var latestVer *semver.Version 412 for _, tag := range tags { 413 if !strings.HasPrefix(tag, "v") { 414 continue 415 } 416 curVer, err := semver.NewVersion(tag) 417 // Skip tags that are not valid semantic versions 418 if err != nil { 419 continue 420 } 421 // Skip pre-releases 422 if len(curVer.Prerelease()) != 0 { 423 continue 424 } 425 // Skip versions that don't match our constraints 426 if !verCon.Check(curVer) { 427 continue 428 } 429 if latestVer == nil || curVer.GreaterThan(latestVer) { 430 latestVer = curVer 431 } 432 } 433 // No relevant latest version (will cause the generator to only use the tip 434 // of the current branch) 435 if latestVer == nil { 436 return "", nil 437 } 438 // Ensure the version string has a "v" prefix, because all CometBFT E2E 439 // node Docker images' versions have a "v" prefix. 440 vs := latestVer.String() 441 if !strings.HasPrefix(vs, "v") { 442 return "v" + vs, nil 443 } 444 return vs, nil 445 }