github.com/consideritdone/landslidecore@v0.0.0-20230718131026-a8b21c5cf8a7/test/e2e/runner/setup.go (about) 1 // nolint: gosec 2 package main 3 4 import ( 5 "bytes" 6 "encoding/base64" 7 "encoding/json" 8 "errors" 9 "fmt" 10 "io/ioutil" 11 "os" 12 "path/filepath" 13 "regexp" 14 "sort" 15 "strconv" 16 "strings" 17 "text/template" 18 "time" 19 20 "github.com/BurntSushi/toml" 21 22 "github.com/consideritdone/landslidecore/config" 23 "github.com/consideritdone/landslidecore/crypto/ed25519" 24 "github.com/consideritdone/landslidecore/p2p" 25 "github.com/consideritdone/landslidecore/privval" 26 e2e "github.com/consideritdone/landslidecore/test/e2e/pkg" 27 "github.com/consideritdone/landslidecore/types" 28 ) 29 30 const ( 31 AppAddressTCP = "tcp://127.0.0.1:30000" 32 AppAddressUNIX = "unix:///var/run/app.sock" 33 34 PrivvalAddressTCP = "tcp://0.0.0.0:27559" 35 PrivvalAddressUNIX = "unix:///var/run/privval.sock" 36 PrivvalKeyFile = "config/priv_validator_key.json" 37 PrivvalStateFile = "data/priv_validator_state.json" 38 PrivvalDummyKeyFile = "config/dummy_validator_key.json" 39 PrivvalDummyStateFile = "data/dummy_validator_state.json" 40 ) 41 42 // Setup sets up the testnet configuration. 43 func Setup(testnet *e2e.Testnet) error { 44 logger.Info(fmt.Sprintf("Generating testnet files in %q", testnet.Dir)) 45 46 err := os.MkdirAll(testnet.Dir, os.ModePerm) 47 if err != nil { 48 return err 49 } 50 51 compose, err := MakeDockerCompose(testnet) 52 if err != nil { 53 return err 54 } 55 err = ioutil.WriteFile(filepath.Join(testnet.Dir, "docker-compose.yml"), compose, 0644) 56 if err != nil { 57 return err 58 } 59 60 genesis, err := MakeGenesis(testnet) 61 if err != nil { 62 return err 63 } 64 65 for _, node := range testnet.Nodes { 66 nodeDir := filepath.Join(testnet.Dir, node.Name) 67 68 dirs := []string{ 69 filepath.Join(nodeDir, "config"), 70 filepath.Join(nodeDir, "data"), 71 filepath.Join(nodeDir, "data", "app"), 72 } 73 for _, dir := range dirs { 74 // light clients don't need an app directory 75 if node.Mode == e2e.ModeLight && strings.Contains(dir, "app") { 76 continue 77 } 78 err := os.MkdirAll(dir, 0755) 79 if err != nil { 80 return err 81 } 82 } 83 84 cfg, err := MakeConfig(node) 85 if err != nil { 86 return err 87 } 88 config.WriteConfigFile(filepath.Join(nodeDir, "config", "config.toml"), cfg) // panics 89 90 appCfg, err := MakeAppConfig(node) 91 if err != nil { 92 return err 93 } 94 err = ioutil.WriteFile(filepath.Join(nodeDir, "config", "app.toml"), appCfg, 0644) 95 if err != nil { 96 return err 97 } 98 99 if node.Mode == e2e.ModeLight { 100 // stop early if a light client 101 continue 102 } 103 104 err = genesis.SaveAs(filepath.Join(nodeDir, "config", "genesis.json")) 105 if err != nil { 106 return err 107 } 108 109 err = (&p2p.NodeKey{PrivKey: node.NodeKey}).SaveAs(filepath.Join(nodeDir, "config", "node_key.json")) 110 if err != nil { 111 return err 112 } 113 114 (privval.NewFilePV(node.PrivvalKey, 115 filepath.Join(nodeDir, PrivvalKeyFile), 116 filepath.Join(nodeDir, PrivvalStateFile), 117 )).Save() 118 119 // Set up a dummy validator. Tendermint requires a file PV even when not used, so we 120 // give it a dummy such that it will fail if it actually tries to use it. 121 (privval.NewFilePV(ed25519.GenPrivKey(), 122 filepath.Join(nodeDir, PrivvalDummyKeyFile), 123 filepath.Join(nodeDir, PrivvalDummyStateFile), 124 )).Save() 125 } 126 127 return nil 128 } 129 130 // MakeDockerCompose generates a Docker Compose config for a testnet. 131 func MakeDockerCompose(testnet *e2e.Testnet) ([]byte, error) { 132 // Must use version 2 Docker Compose format, to support IPv6. 133 tmpl, err := template.New("docker-compose").Funcs(template.FuncMap{ 134 "misbehaviorsToString": func(misbehaviors map[int64]string) string { 135 str := "" 136 for height, misbehavior := range misbehaviors { 137 // after the first behavior set, a comma must be prepended 138 if str != "" { 139 str += "," 140 } 141 heightString := strconv.Itoa(int(height)) 142 str += misbehavior + "," + heightString 143 } 144 return str 145 }, 146 }).Parse(`version: '2.4' 147 148 networks: 149 {{ .Name }}: 150 labels: 151 e2e: true 152 driver: bridge 153 {{- if .IPv6 }} 154 enable_ipv6: true 155 {{- end }} 156 ipam: 157 driver: default 158 config: 159 - subnet: {{ .IP }} 160 161 services: 162 {{- range .Nodes }} 163 {{ .Name }}: 164 labels: 165 e2e: true 166 container_name: {{ .Name }} 167 image: tendermint/e2e-node 168 {{- if eq .ABCIProtocol "builtin" }} 169 entrypoint: /usr/bin/entrypoint-builtin 170 {{- else if .Misbehaviors }} 171 entrypoint: /usr/bin/entrypoint-maverick 172 command: ["node", "--misbehaviors", "{{ misbehaviorsToString .Misbehaviors }}"] 173 {{- end }} 174 init: true 175 ports: 176 - 26656 177 - {{ if .ProxyPort }}{{ .ProxyPort }}:{{ end }}26657 178 - 6060 179 volumes: 180 - ./{{ .Name }}:/tendermint 181 networks: 182 {{ $.Name }}: 183 ipv{{ if $.IPv6 }}6{{ else }}4{{ end}}_address: {{ .IP }} 184 185 {{end}}`) 186 if err != nil { 187 return nil, err 188 } 189 var buf bytes.Buffer 190 err = tmpl.Execute(&buf, testnet) 191 if err != nil { 192 return nil, err 193 } 194 return buf.Bytes(), nil 195 } 196 197 // MakeGenesis generates a genesis document. 198 func MakeGenesis(testnet *e2e.Testnet) (types.GenesisDoc, error) { 199 genesis := types.GenesisDoc{ 200 GenesisTime: time.Now(), 201 ChainID: testnet.Name, 202 ConsensusParams: types.DefaultConsensusParams(), 203 InitialHeight: testnet.InitialHeight, 204 } 205 // set the app version to 1 206 genesis.ConsensusParams.Version.AppVersion = 1 207 for validator, power := range testnet.Validators { 208 genesis.Validators = append(genesis.Validators, types.GenesisValidator{ 209 Name: validator.Name, 210 Address: validator.PrivvalKey.PubKey().Address(), 211 PubKey: validator.PrivvalKey.PubKey(), 212 Power: power, 213 }) 214 } 215 // The validator set will be sorted internally by Tendermint ranked by power, 216 // but we sort it here as well so that all genesis files are identical. 217 sort.Slice(genesis.Validators, func(i, j int) bool { 218 return strings.Compare(genesis.Validators[i].Name, genesis.Validators[j].Name) == -1 219 }) 220 if len(testnet.InitialState) > 0 { 221 appState, err := json.Marshal(testnet.InitialState) 222 if err != nil { 223 return genesis, err 224 } 225 genesis.AppState = appState 226 } 227 return genesis, genesis.ValidateAndComplete() 228 } 229 230 // MakeConfig generates a Tendermint config for a node. 231 func MakeConfig(node *e2e.Node) (*config.Config, error) { 232 cfg := config.DefaultConfig() 233 cfg.Moniker = node.Name 234 cfg.ProxyApp = AppAddressTCP 235 cfg.RPC.ListenAddress = "tcp://0.0.0.0:26657" 236 cfg.RPC.PprofListenAddress = ":6060" 237 cfg.P2P.ExternalAddress = fmt.Sprintf("tcp://%v", node.AddressP2P(false)) 238 cfg.P2P.AddrBookStrict = false 239 cfg.DBBackend = node.Database 240 cfg.StateSync.DiscoveryTime = 5 * time.Second 241 242 switch node.ABCIProtocol { 243 case e2e.ProtocolUNIX: 244 cfg.ProxyApp = AppAddressUNIX 245 case e2e.ProtocolTCP: 246 cfg.ProxyApp = AppAddressTCP 247 case e2e.ProtocolGRPC: 248 cfg.ProxyApp = AppAddressTCP 249 cfg.ABCI = "grpc" 250 case e2e.ProtocolBuiltin: 251 cfg.ProxyApp = "" 252 cfg.ABCI = "" 253 default: 254 return nil, fmt.Errorf("unexpected ABCI protocol setting %q", node.ABCIProtocol) 255 } 256 257 // Tendermint errors if it does not have a privval key set up, regardless of whether 258 // it's actually needed (e.g. for remote KMS or non-validators). We set up a dummy 259 // key here by default, and use the real key for actual validators that should use 260 // the file privval. 261 cfg.PrivValidatorListenAddr = "" 262 cfg.PrivValidatorKey = PrivvalDummyKeyFile 263 cfg.PrivValidatorState = PrivvalDummyStateFile 264 265 switch node.Mode { 266 case e2e.ModeValidator: 267 switch node.PrivvalProtocol { 268 case e2e.ProtocolFile: 269 cfg.PrivValidatorKey = PrivvalKeyFile 270 cfg.PrivValidatorState = PrivvalStateFile 271 case e2e.ProtocolUNIX: 272 cfg.PrivValidatorListenAddr = PrivvalAddressUNIX 273 case e2e.ProtocolTCP: 274 cfg.PrivValidatorListenAddr = PrivvalAddressTCP 275 default: 276 return nil, fmt.Errorf("invalid privval protocol setting %q", node.PrivvalProtocol) 277 } 278 case e2e.ModeSeed: 279 cfg.P2P.SeedMode = true 280 cfg.P2P.PexReactor = true 281 case e2e.ModeFull, e2e.ModeLight: 282 // Don't need to do anything, since we're using a dummy privval key by default. 283 default: 284 return nil, fmt.Errorf("unexpected mode %q", node.Mode) 285 } 286 287 if node.FastSync == "" { 288 cfg.FastSyncMode = false 289 } else { 290 cfg.FastSync.Version = node.FastSync 291 } 292 293 if node.StateSync { 294 cfg.StateSync.Enable = true 295 cfg.StateSync.RPCServers = []string{} 296 for _, peer := range node.Testnet.ArchiveNodes() { 297 if peer.Name == node.Name { 298 continue 299 } 300 cfg.StateSync.RPCServers = append(cfg.StateSync.RPCServers, peer.AddressRPC()) 301 } 302 if len(cfg.StateSync.RPCServers) < 2 { 303 return nil, errors.New("unable to find 2 suitable state sync RPC servers") 304 } 305 } 306 307 cfg.P2P.Seeds = "" 308 for _, seed := range node.Seeds { 309 if len(cfg.P2P.Seeds) > 0 { 310 cfg.P2P.Seeds += "," 311 } 312 cfg.P2P.Seeds += seed.AddressP2P(true) 313 } 314 cfg.P2P.PersistentPeers = "" 315 for _, peer := range node.PersistentPeers { 316 if len(cfg.P2P.PersistentPeers) > 0 { 317 cfg.P2P.PersistentPeers += "," 318 } 319 cfg.P2P.PersistentPeers += peer.AddressP2P(true) 320 } 321 return cfg, nil 322 } 323 324 // MakeAppConfig generates an ABCI application config for a node. 325 func MakeAppConfig(node *e2e.Node) ([]byte, error) { 326 cfg := map[string]interface{}{ 327 "chain_id": node.Testnet.Name, 328 "dir": "data/app", 329 "listen": AppAddressUNIX, 330 "mode": node.Mode, 331 "proxy_port": node.ProxyPort, 332 "protocol": "socket", 333 "persist_interval": node.PersistInterval, 334 "snapshot_interval": node.SnapshotInterval, 335 "retain_blocks": node.RetainBlocks, 336 "key_type": node.PrivvalKey.Type(), 337 } 338 switch node.ABCIProtocol { 339 case e2e.ProtocolUNIX: 340 cfg["listen"] = AppAddressUNIX 341 case e2e.ProtocolTCP: 342 cfg["listen"] = AppAddressTCP 343 case e2e.ProtocolGRPC: 344 cfg["listen"] = AppAddressTCP 345 cfg["protocol"] = "grpc" 346 case e2e.ProtocolBuiltin: 347 delete(cfg, "listen") 348 cfg["protocol"] = "builtin" 349 default: 350 return nil, fmt.Errorf("unexpected ABCI protocol setting %q", node.ABCIProtocol) 351 } 352 if node.Mode == e2e.ModeValidator { 353 switch node.PrivvalProtocol { 354 case e2e.ProtocolFile: 355 case e2e.ProtocolTCP: 356 cfg["privval_server"] = PrivvalAddressTCP 357 cfg["privval_key"] = PrivvalKeyFile 358 cfg["privval_state"] = PrivvalStateFile 359 case e2e.ProtocolUNIX: 360 cfg["privval_server"] = PrivvalAddressUNIX 361 cfg["privval_key"] = PrivvalKeyFile 362 cfg["privval_state"] = PrivvalStateFile 363 default: 364 return nil, fmt.Errorf("unexpected privval protocol setting %q", node.PrivvalProtocol) 365 } 366 } 367 368 misbehaviors := make(map[string]string) 369 for height, misbehavior := range node.Misbehaviors { 370 misbehaviors[strconv.Itoa(int(height))] = misbehavior 371 } 372 cfg["misbehaviors"] = misbehaviors 373 374 if len(node.Testnet.ValidatorUpdates) > 0 { 375 validatorUpdates := map[string]map[string]int64{} 376 for height, validators := range node.Testnet.ValidatorUpdates { 377 updateVals := map[string]int64{} 378 for node, power := range validators { 379 updateVals[base64.StdEncoding.EncodeToString(node.PrivvalKey.PubKey().Bytes())] = power 380 } 381 validatorUpdates[fmt.Sprintf("%v", height)] = updateVals 382 } 383 cfg["validator_update"] = validatorUpdates 384 } 385 386 var buf bytes.Buffer 387 err := toml.NewEncoder(&buf).Encode(cfg) 388 if err != nil { 389 return nil, fmt.Errorf("failed to generate app config: %w", err) 390 } 391 return buf.Bytes(), nil 392 } 393 394 // UpdateConfigStateSync updates the state sync config for a node. 395 func UpdateConfigStateSync(node *e2e.Node, height int64, hash []byte) error { 396 cfgPath := filepath.Join(node.Testnet.Dir, node.Name, "config", "config.toml") 397 398 // FIXME Apparently there's no function to simply load a config file without 399 // involving the entire Viper apparatus, so we'll just resort to regexps. 400 bz, err := ioutil.ReadFile(cfgPath) 401 if err != nil { 402 return err 403 } 404 bz = regexp.MustCompile(`(?m)^trust_height =.*`).ReplaceAll(bz, []byte(fmt.Sprintf(`trust_height = %v`, height))) 405 bz = regexp.MustCompile(`(?m)^trust_hash =.*`).ReplaceAll(bz, []byte(fmt.Sprintf(`trust_hash = "%X"`, hash))) 406 return ioutil.WriteFile(cfgPath, bz, 0644) 407 }