// nolint: gosec package main import ( "bytes" "encoding/base64" "encoding/json" "errors" "fmt" "os" "path/filepath" "regexp" "sort" "strings" "text/template" "time" "github.com/BurntSushi/toml" "github.com/tendermint/tendermint/config" "github.com/tendermint/tendermint/crypto/ed25519" "github.com/tendermint/tendermint/privval" e2e "github.com/tendermint/tendermint/test/e2e/pkg" "github.com/tendermint/tendermint/types" ) const ( AppAddressTCP = "tcp://127.0.0.1:30000" AppAddressUNIX = "unix:///var/run/app.sock" PrivvalAddressTCP = "tcp://0.0.0.0:27559" PrivvalAddressGRPC = "grpc://0.0.0.0:27559" PrivvalAddressUNIX = "unix:///var/run/privval.sock" PrivvalKeyFile = "config/priv_validator_key.json" PrivvalStateFile = "data/priv_validator_state.json" PrivvalDummyKeyFile = "config/dummy_validator_key.json" PrivvalDummyStateFile = "data/dummy_validator_state.json" ) // Setup sets up the testnet configuration. func Setup(testnet *e2e.Testnet) error { logger.Info(fmt.Sprintf("Generating testnet files in %q", testnet.Dir)) err := os.MkdirAll(testnet.Dir, os.ModePerm) if err != nil { return err } compose, err := MakeDockerCompose(testnet) if err != nil { return err } err = os.WriteFile(filepath.Join(testnet.Dir, "docker-compose.yml"), compose, 0644) if err != nil { return err } genesis, err := MakeGenesis(testnet) if err != nil { return err } for _, node := range testnet.Nodes { nodeDir := filepath.Join(testnet.Dir, node.Name) dirs := []string{ filepath.Join(nodeDir, "config"), filepath.Join(nodeDir, "data"), filepath.Join(nodeDir, "data", "app"), } for _, dir := range dirs { // light clients don't need an app directory if node.Mode == e2e.ModeLight && strings.Contains(dir, "app") { continue } err := os.MkdirAll(dir, 0755) if err != nil { return err } } cfg, err := MakeConfig(node) if err != nil { return err } if err := config.WriteConfigFile(nodeDir, cfg); err != nil { return err } appCfg, err := MakeAppConfig(node) if err != nil { return err } err = os.WriteFile(filepath.Join(nodeDir, "config", "app.toml"), appCfg, 0644) if err != nil { return err } if node.Mode == e2e.ModeLight { // stop early if a light client continue } err = genesis.SaveAs(filepath.Join(nodeDir, "config", "genesis.json")) if err != nil { return err } err = (&types.NodeKey{PrivKey: node.NodeKey}).SaveAs(filepath.Join(nodeDir, "config", "node_key.json")) if err != nil { return err } err = (privval.NewFilePV(node.PrivvalKey, filepath.Join(nodeDir, PrivvalKeyFile), filepath.Join(nodeDir, PrivvalStateFile), )).Save() if err != nil { return err } // Set up a dummy validator. Tendermint requires a file PV even when not used, so we // give it a dummy such that it will fail if it actually tries to use it. err = (privval.NewFilePV(ed25519.GenPrivKey(), filepath.Join(nodeDir, PrivvalDummyKeyFile), filepath.Join(nodeDir, PrivvalDummyStateFile), )).Save() if err != nil { return err } } return nil } // MakeDockerCompose generates a Docker Compose config for a testnet. func MakeDockerCompose(testnet *e2e.Testnet) ([]byte, error) { // Must use version 2 Docker Compose format, to support IPv6. tmpl, err := template.New("docker-compose").Funcs(template.FuncMap{ "addUint32": func(x, y uint32) uint32 { return x + y }, }).Parse(`version: '2.4' networks: {{ .Name }}: labels: e2e: true driver: bridge {{- if .IPv6 }} enable_ipv6: true {{- end }} ipam: driver: default config: - subnet: {{ .IP }} services: {{- range .Nodes }} {{ .Name }}: labels: e2e: true container_name: {{ .Name }} image: tendermint/e2e-node {{- if eq .ABCIProtocol "builtin" }} entrypoint: /usr/bin/entrypoint-builtin {{- else if .LogLevel }} command: start --log-level {{ .LogLevel }} {{- end }} init: true ports: - 26656 - {{ if .ProxyPort }}{{ addUint32 .ProxyPort 1000 }}:{{ end }}26660 - {{ if .ProxyPort }}{{ .ProxyPort }}:{{ end }}26657 - 6060 volumes: - ./{{ .Name }}:/tendermint networks: {{ $.Name }}: ipv{{ if $.IPv6 }}6{{ else }}4{{ end}}_address: {{ .IP }} {{end}}`) if err != nil { return nil, err } var buf bytes.Buffer err = tmpl.Execute(&buf, testnet) if err != nil { return nil, err } return buf.Bytes(), nil } // MakeGenesis generates a genesis document. func MakeGenesis(testnet *e2e.Testnet) (types.GenesisDoc, error) { genesis := types.GenesisDoc{ GenesisTime: time.Now(), ChainID: testnet.Name, ConsensusParams: types.DefaultConsensusParams(), InitialHeight: testnet.InitialHeight, } switch testnet.KeyType { case "", types.ABCIPubKeyTypeEd25519, types.ABCIPubKeyTypeSecp256k1: genesis.ConsensusParams.Validator.PubKeyTypes = append(genesis.ConsensusParams.Validator.PubKeyTypes, types.ABCIPubKeyTypeSecp256k1) default: return genesis, errors.New("unsupported KeyType") } genesis.ConsensusParams.Evidence.MaxAgeNumBlocks = e2e.EvidenceAgeHeight genesis.ConsensusParams.Evidence.MaxAgeDuration = e2e.EvidenceAgeTime for validator, power := range testnet.Validators { genesis.Validators = append(genesis.Validators, types.GenesisValidator{ Name: validator.Name, Address: validator.PrivvalKey.PubKey().Address(), PubKey: validator.PrivvalKey.PubKey(), Power: power, }) } // The validator set will be sorted internally by Tendermint ranked by power, // but we sort it here as well so that all genesis files are identical. sort.Slice(genesis.Validators, func(i, j int) bool { return strings.Compare(genesis.Validators[i].Name, genesis.Validators[j].Name) == -1 }) if len(testnet.InitialState) > 0 { appState, err := json.Marshal(testnet.InitialState) if err != nil { return genesis, err } genesis.AppState = appState } return genesis, genesis.ValidateAndComplete() } // MakeConfig generates a Tendermint config for a node. func MakeConfig(node *e2e.Node) (*config.Config, error) { cfg := config.DefaultConfig() cfg.Moniker = node.Name cfg.ProxyApp = AppAddressTCP if node.LogLevel != "" { cfg.LogLevel = node.LogLevel } cfg.RPC.ListenAddress = "tcp://0.0.0.0:26657" cfg.RPC.PprofListenAddress = ":6060" cfg.P2P.ExternalAddress = fmt.Sprintf("tcp://%v", node.AddressP2P(false)) cfg.P2P.QueueType = node.QueueType cfg.DBBackend = node.Database cfg.StateSync.DiscoveryTime = 5 * time.Second if node.Mode != e2e.ModeLight { cfg.Mode = string(node.Mode) } switch node.ABCIProtocol { case e2e.ProtocolUNIX: cfg.ProxyApp = AppAddressUNIX case e2e.ProtocolTCP: cfg.ProxyApp = AppAddressTCP case e2e.ProtocolGRPC: cfg.ProxyApp = AppAddressTCP cfg.ABCI = "grpc" case e2e.ProtocolBuiltin: cfg.ProxyApp = "" cfg.ABCI = "" default: return nil, fmt.Errorf("unexpected ABCI protocol setting %q", node.ABCIProtocol) } // Tendermint errors if it does not have a privval key set up, regardless of whether // it's actually needed (e.g. for remote KMS or non-validators). We set up a dummy // key here by default, and use the real key for actual validators that should use // the file privval. cfg.PrivValidator.ListenAddr = "" cfg.PrivValidator.Key = PrivvalDummyKeyFile cfg.PrivValidator.State = PrivvalDummyStateFile switch node.Mode { case e2e.ModeValidator: switch node.PrivvalProtocol { case e2e.ProtocolFile: cfg.PrivValidator.Key = PrivvalKeyFile cfg.PrivValidator.State = PrivvalStateFile case e2e.ProtocolUNIX: cfg.PrivValidator.ListenAddr = PrivvalAddressUNIX case e2e.ProtocolTCP: cfg.PrivValidator.ListenAddr = PrivvalAddressTCP case e2e.ProtocolGRPC: cfg.PrivValidator.ListenAddr = PrivvalAddressGRPC default: return nil, fmt.Errorf("invalid privval protocol setting %q", node.PrivvalProtocol) } case e2e.ModeSeed: cfg.P2P.PexReactor = true case e2e.ModeFull, e2e.ModeLight: // Don't need to do anything, since we're using a dummy privval key by default. default: return nil, fmt.Errorf("unexpected mode %q", node.Mode) } switch node.StateSync { case e2e.StateSyncP2P: cfg.StateSync.Enable = true cfg.StateSync.UseP2P = true case e2e.StateSyncRPC: cfg.StateSync.Enable = true cfg.StateSync.RPCServers = []string{} for _, peer := range node.Testnet.ArchiveNodes() { if peer.Name == node.Name { continue } cfg.StateSync.RPCServers = append(cfg.StateSync.RPCServers, peer.AddressRPC()) } if len(cfg.StateSync.RPCServers) < 2 { return nil, errors.New("unable to find 2 suitable state sync RPC servers") } } cfg.P2P.Seeds = "" //nolint: staticcheck for _, seed := range node.Seeds { if len(cfg.P2P.Seeds) > 0 { //nolint: staticcheck cfg.P2P.Seeds += "," //nolint: staticcheck } cfg.P2P.Seeds += seed.AddressP2P(true) //nolint: staticcheck } cfg.P2P.PersistentPeers = "" for _, peer := range node.PersistentPeers { if len(cfg.P2P.PersistentPeers) > 0 { cfg.P2P.PersistentPeers += "," } cfg.P2P.PersistentPeers += peer.AddressP2P(true) } cfg.Instrumentation.Prometheus = true return cfg, nil } // MakeAppConfig generates an ABCI application config for a node. func MakeAppConfig(node *e2e.Node) ([]byte, error) { cfg := map[string]interface{}{ "chain_id": node.Testnet.Name, "dir": "data/app", "listen": AppAddressUNIX, "mode": node.Mode, "proxy_port": node.ProxyPort, "protocol": "socket", "persist_interval": node.PersistInterval, "snapshot_interval": node.SnapshotInterval, "retain_blocks": node.RetainBlocks, "key_type": node.PrivvalKey.Type(), } switch node.ABCIProtocol { case e2e.ProtocolUNIX: cfg["listen"] = AppAddressUNIX case e2e.ProtocolTCP: cfg["listen"] = AppAddressTCP case e2e.ProtocolGRPC: cfg["listen"] = AppAddressTCP cfg["protocol"] = "grpc" case e2e.ProtocolBuiltin: delete(cfg, "listen") cfg["protocol"] = "builtin" default: return nil, fmt.Errorf("unexpected ABCI protocol setting %q", node.ABCIProtocol) } if node.Mode == e2e.ModeValidator { switch node.PrivvalProtocol { case e2e.ProtocolFile: case e2e.ProtocolTCP: cfg["privval_server"] = PrivvalAddressTCP cfg["privval_key"] = PrivvalKeyFile cfg["privval_state"] = PrivvalStateFile case e2e.ProtocolUNIX: cfg["privval_server"] = PrivvalAddressUNIX cfg["privval_key"] = PrivvalKeyFile cfg["privval_state"] = PrivvalStateFile case e2e.ProtocolGRPC: cfg["privval_server"] = PrivvalAddressGRPC cfg["privval_key"] = PrivvalKeyFile cfg["privval_state"] = PrivvalStateFile default: return nil, fmt.Errorf("unexpected privval protocol setting %q", node.PrivvalProtocol) } } if len(node.Testnet.ValidatorUpdates) > 0 { validatorUpdates := map[string]map[string]int64{} for height, validators := range node.Testnet.ValidatorUpdates { updateVals := map[string]int64{} for node, power := range validators { updateVals[base64.StdEncoding.EncodeToString(node.PrivvalKey.PubKey().Bytes())] = power } validatorUpdates[fmt.Sprintf("%v", height)] = updateVals } cfg["validator_update"] = validatorUpdates } var buf bytes.Buffer err := toml.NewEncoder(&buf).Encode(cfg) if err != nil { return nil, fmt.Errorf("failed to generate app config: %w", err) } return buf.Bytes(), nil } // UpdateConfigStateSync updates the state sync config for a node. func UpdateConfigStateSync(node *e2e.Node, height int64, hash []byte) error { cfgPath := filepath.Join(node.Testnet.Dir, node.Name, "config", "config.toml") // FIXME Apparently there's no function to simply load a config file without // involving the entire Viper apparatus, so we'll just resort to regexps. bz, err := os.ReadFile(cfgPath) if err != nil { return err } bz = regexp.MustCompile(`(?m)^trust-height =.*`).ReplaceAll(bz, []byte(fmt.Sprintf(`trust-height = %v`, height))) bz = regexp.MustCompile(`(?m)^trust-hash =.*`).ReplaceAll(bz, []byte(fmt.Sprintf(`trust-hash = "%X"`, hash))) return os.WriteFile(cfgPath, bz, 0644) }