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