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