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  }