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