mirror of
https://github.com/luxfi/cli.git
synced 2026-10-30 17:01:20 +00:00
493 lines
16 KiB
Go
493 lines
16 KiB
Go
// Copyright (C) 2022, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
package networkcmd
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path"
|
|
"time"
|
|
|
|
"github.com/luxfi/cli/v2/pkg/binutils"
|
|
"github.com/luxfi/cli/v2/pkg/constants"
|
|
"github.com/luxfi/cli/v2/pkg/models"
|
|
"github.com/luxfi/cli/v2/pkg/subnet"
|
|
"github.com/luxfi/cli/v2/pkg/ux"
|
|
"github.com/luxfi/cli/v2/pkg/vm"
|
|
"github.com/luxfi/netrunner/client"
|
|
"github.com/luxfi/netrunner/server"
|
|
"github.com/luxfi/netrunner/utils"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
var (
|
|
userProvidedLuxVersion string
|
|
snapshotName string
|
|
mainnet bool
|
|
testnet bool
|
|
// BadgerDB flags
|
|
dbEngine string
|
|
archiveDir string
|
|
archiveShared bool
|
|
genesisImport string
|
|
)
|
|
|
|
const latest = "latest"
|
|
|
|
func newStartCmd() *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "start",
|
|
Short: "Starts a local network",
|
|
Long: `The network start command starts a local, multi-node Lux network on your machine.
|
|
|
|
By default, the command loads the default snapshot. If you provide the --snapshot-name
|
|
flag, the network loads that snapshot instead. The command fails if the local network is
|
|
already running.`,
|
|
|
|
RunE: StartNetwork,
|
|
Args: cobra.ExactArgs(0),
|
|
SilenceUsage: true,
|
|
}
|
|
|
|
cmd.Flags().StringVar(&userProvidedLuxVersion, "node-version", latest, "use this version of node (ex: v1.17.12)")
|
|
cmd.Flags().StringVar(&snapshotName, "snapshot-name", constants.DefaultSnapshotName, "name of snapshot to use to start the network from")
|
|
cmd.Flags().BoolVar(&mainnet, "mainnet", false, "start a mainnet node with 21 validators")
|
|
cmd.Flags().BoolVar(&testnet, "testnet", false, "start a testnet node with 11 validators")
|
|
// BadgerDB flags
|
|
cmd.Flags().StringVar(&dbEngine, "db-backend", "", "database backend to use (pebble, leveldb, or badgerdb)")
|
|
cmd.Flags().StringVar(&archiveDir, "archive-path", "", "path to BadgerDB archive database (enables dual-database mode)")
|
|
cmd.Flags().BoolVar(&archiveShared, "archive-shared", false, "enable shared read-only access to archive database")
|
|
cmd.Flags().StringVar(&genesisImport, "genesis-path", "", "path to genesis database to import (PebbleDB or LevelDB)")
|
|
|
|
return cmd
|
|
}
|
|
|
|
func StartNetwork(*cobra.Command, []string) error {
|
|
// Check for conflicting flags
|
|
if mainnet && testnet {
|
|
return fmt.Errorf("cannot use both --mainnet and --testnet flags")
|
|
}
|
|
|
|
// If mainnet or testnet flag is set, delegate to the appropriate function
|
|
if mainnet {
|
|
return StartMainnet()
|
|
}
|
|
if testnet {
|
|
return StartTestnet()
|
|
}
|
|
luxVersion, err := determineLuxVersion(userProvidedLuxVersion)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
sd := subnet.NewLocalDeployer(app, luxVersion, "")
|
|
|
|
if err := sd.StartServer(); err != nil {
|
|
return err
|
|
}
|
|
|
|
nodeBinPath, err := sd.SetupLocalEnv()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
cli, err := binutils.NewGRPCClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var startMsg string
|
|
if snapshotName == constants.DefaultSnapshotName {
|
|
startMsg = "Starting previously deployed and stopped snapshot"
|
|
} else {
|
|
startMsg = fmt.Sprintf("Starting previously deployed and stopped snapshot %s...", snapshotName)
|
|
}
|
|
ux.Logger.PrintToUser("%s", startMsg)
|
|
|
|
outputDirPrefix := path.Join(app.GetRunDir(), "restart")
|
|
outputDir, err := utils.MkDirWithTimestamp(outputDirPrefix)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
pluginDir := app.GetPluginsDir()
|
|
|
|
loadSnapshotOpts := []client.OpOption{
|
|
client.WithExecPath(nodeBinPath),
|
|
client.WithRootDataDir(outputDir),
|
|
client.WithReassignPortsIfUsed(true),
|
|
client.WithPluginDir(pluginDir),
|
|
}
|
|
|
|
// load global node configs if they exist
|
|
configStr, err := app.Conf.LoadNodeConfig()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Build node config with BadgerDB options
|
|
nodeConfig := make(map[string]interface{})
|
|
if configStr != "" {
|
|
if err := json.Unmarshal([]byte(configStr), &nodeConfig); err != nil {
|
|
return fmt.Errorf("invalid node config: %w", err)
|
|
}
|
|
}
|
|
|
|
// Add BadgerDB configuration if specified
|
|
if dbEngine != "" {
|
|
nodeConfig["db-engine"] = dbEngine
|
|
}
|
|
if archiveDir != "" {
|
|
nodeConfig["archive-dir"] = archiveDir
|
|
nodeConfig["archive-shared"] = archiveShared
|
|
}
|
|
if genesisImport != "" {
|
|
nodeConfig["genesis-import"] = genesisImport
|
|
nodeConfig["genesis-replay"] = true
|
|
nodeConfig["genesis-verify"] = true
|
|
}
|
|
|
|
// Convert back to JSON
|
|
if len(nodeConfig) > 0 {
|
|
updatedConfigBytes, err := json.Marshal(nodeConfig)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal node config: %w", err)
|
|
}
|
|
loadSnapshotOpts = append(loadSnapshotOpts, client.WithGlobalNodeConfig(string(updatedConfigBytes)))
|
|
}
|
|
|
|
ctx := binutils.GetAsyncContext()
|
|
|
|
pp, err := cli.LoadSnapshot(
|
|
ctx,
|
|
snapshotName,
|
|
loadSnapshotOpts...,
|
|
)
|
|
|
|
if err != nil {
|
|
if !server.IsServerError(err, server.ErrAlreadyBootstrapped) {
|
|
return fmt.Errorf("failed to start network with the persisted snapshot: %w", err)
|
|
}
|
|
ux.Logger.PrintToUser("Network has already been booted. Wait until healthy...")
|
|
} else {
|
|
ux.Logger.PrintToUser("Booting Network. Wait until healthy...")
|
|
ux.Logger.PrintToUser("Node log path: %s/node<i>/logs", pp.ClusterInfo.RootDataDir)
|
|
}
|
|
|
|
clusterInfo, err := subnet.WaitForHealthy(ctx, cli)
|
|
if err != nil {
|
|
return fmt.Errorf("failed waiting for network to become healthy: %w", err)
|
|
}
|
|
|
|
fmt.Println()
|
|
if subnet.HasEndpoints(clusterInfo) {
|
|
ux.Logger.PrintToUser("Network ready to use. Local network node endpoints:")
|
|
ux.PrintTableEndpoints(clusterInfo)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func determineLuxVersion(userProvidedLuxVersion string) (string, error) {
|
|
// a specific user provided version should override this calculation, so just return
|
|
if userProvidedLuxVersion != latest {
|
|
return userProvidedLuxVersion, nil
|
|
}
|
|
|
|
// Need to determine which subnets have been deployed
|
|
locallyDeployedSubnets, err := subnet.GetLocallyDeployedSubnetsFromFile(app)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// if no subnets have been deployed, use latest
|
|
if len(locallyDeployedSubnets) == 0 {
|
|
return latest, nil
|
|
}
|
|
|
|
currentRPCVersion := -1
|
|
|
|
// For each deployed subnet, check RPC versions
|
|
for _, deployedSubnet := range locallyDeployedSubnets {
|
|
sc, err := app.LoadSidecar(deployedSubnet)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// if you have a custom vm, you must provide the version explicitly
|
|
// if you upgrade from evm to a custom vm, the RPC version will be 0
|
|
if sc.VM == models.CustomVM || sc.Networks[models.Local.String()].RPCVersion == 0 {
|
|
continue
|
|
}
|
|
|
|
if currentRPCVersion == -1 {
|
|
currentRPCVersion = sc.Networks[models.Local.String()].RPCVersion
|
|
}
|
|
|
|
if sc.Networks[models.Local.String()].RPCVersion != currentRPCVersion {
|
|
return "", fmt.Errorf(
|
|
"RPC version mismatch. Expected %d, got %d for Subnet %s. Upgrade all subnets to the same RPC version to launch the network",
|
|
currentRPCVersion,
|
|
sc.RPCVersion,
|
|
sc.Name,
|
|
)
|
|
}
|
|
}
|
|
|
|
// If currentRPCVersion == -1, then only custom subnets have been deployed, the user must provide the version explicitly if not latest
|
|
if currentRPCVersion == -1 {
|
|
ux.Logger.PrintToUser("No Subnet RPC version found. Using latest Lux version")
|
|
return latest, nil
|
|
}
|
|
|
|
return vm.GetLatestLuxByProtocolVersion(
|
|
app,
|
|
currentRPCVersion,
|
|
constants.LuxCompatibilityURL,
|
|
)
|
|
}
|
|
|
|
// StartMainnet starts a mainnet network with 21 validator nodes
|
|
func StartMainnet() error {
|
|
ux.Logger.PrintToUser("Starting Lux mainnet with 21 validator nodes...")
|
|
|
|
// Check if local luxd binary exists
|
|
localLuxdPath := "/home/z/work/lux/node/build/luxd"
|
|
if _, err := os.Stat(localLuxdPath); os.IsNotExist(err) {
|
|
return fmt.Errorf("luxd binary not found at %s. Please run 'make build-node' first", localLuxdPath)
|
|
}
|
|
|
|
// Use local binary instead of downloading
|
|
sd := subnet.NewLocalDeployer(app, "", "")
|
|
|
|
// Start netrunner server
|
|
if err := sd.StartServer(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Use local binary path
|
|
nodeBinPath := localLuxdPath
|
|
|
|
// Get gRPC client
|
|
cli, err := binutils.NewGRPCClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Historic chaindata setup
|
|
historicChainData := "/home/z/work/lux/mainnet-data/chainData/C/db"
|
|
if _, err := os.Stat(historicChainData); err == nil {
|
|
ux.Logger.PrintToUser("Found historic C-Chain data at %s", historicChainData)
|
|
// Copy to first validator's data directory will be handled post-launch
|
|
}
|
|
|
|
// Build mainnet configuration with proper consensus parameters
|
|
globalNodeConfig := `{
|
|
"network-id": "96369",
|
|
"db-type": "pebbledb",
|
|
"staking-enabled": true,
|
|
"sybil-protection-enabled": true,
|
|
"health-check-frequency": "30s",
|
|
"http-port": 9630,
|
|
"staking-port": 9631,
|
|
"consensus-sample-size": 21,
|
|
"consensus-quorum-size": 15,
|
|
"consensus-virtuous-commit-threshold": 21,
|
|
"consensus-rogue-commit-threshold": 28,
|
|
"consensus-concurrent-repolls": 3,
|
|
"consensus-optimal-processing": 50,
|
|
"consensus-max-processing": 3000,
|
|
"consensus-max-time-processing": 3000000000,
|
|
"stake-max-consumption-rate": 120000,
|
|
"stake-min-consumption-rate": 100000,
|
|
"stake-minted-reward": 64000000,
|
|
"stake-supply-cap": 3000000000000000,
|
|
"uptime-requirement": 0.8,
|
|
"max-stake-duration": 31536000,
|
|
"min-stake-duration": 1209600,
|
|
"min-validator-stake": 2000000000000,
|
|
"min-delegator-stake": 25000000000,
|
|
"delegation-fee-cap": 200000,
|
|
"stake-minting-period": 31536000,
|
|
"log-level": "info"
|
|
}`
|
|
|
|
// Prepare chain configs with genesis files
|
|
chainConfigs := map[string]string{}
|
|
genesisPath := "/home/z/work/lux/genesis/configs/mainnet"
|
|
|
|
// Read genesis files and add to chain configs
|
|
for _, chain := range []string{"C", "P", "X"} {
|
|
genesisFile := fmt.Sprintf("%s/%s/genesis.json", genesisPath, chain)
|
|
if genesisData, err := os.ReadFile(genesisFile); err == nil {
|
|
chainConfigs[chain] = string(genesisData)
|
|
} else {
|
|
ux.Logger.PrintToUser("Warning: Could not read %s genesis file: %v", chain, err)
|
|
}
|
|
}
|
|
|
|
// Build start options
|
|
rootDataDir := path.Join(app.GetRunDir(), "mainnet-"+time.Now().Format("20060102-150405"))
|
|
|
|
opts := []client.OpOption{
|
|
client.WithExecPath(nodeBinPath),
|
|
client.WithNumNodes(21),
|
|
client.WithGlobalNodeConfig(globalNodeConfig),
|
|
client.WithRootDataDir(rootDataDir),
|
|
client.WithReassignPortsIfUsed(true),
|
|
client.WithDynamicPorts(false), // Use fixed ports starting from 9630
|
|
client.WithChainConfigs(chainConfigs),
|
|
}
|
|
|
|
// Add plugin directory if it exists
|
|
pluginDir := path.Join(app.GetPluginsDir(), "evm")
|
|
if _, err := os.Stat(pluginDir); err == nil {
|
|
opts = append(opts, client.WithPluginDir(pluginDir))
|
|
}
|
|
|
|
ctx := binutils.GetAsyncContext()
|
|
|
|
ux.Logger.PrintToUser("Starting network with 21 validators...")
|
|
ux.Logger.PrintToUser("Network ID: 96369")
|
|
ux.Logger.PrintToUser("Root data directory: %s", rootDataDir)
|
|
|
|
// Start the network
|
|
startResp, err := cli.Start(ctx, path.Join(rootDataDir, "netrunner.log"), opts...)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to start network: %w", err)
|
|
}
|
|
|
|
// Wait for healthy network
|
|
ux.Logger.PrintToUser("Waiting for all validators to become healthy...")
|
|
healthCheckStart := time.Now()
|
|
healthy := false
|
|
|
|
for !healthy && time.Since(healthCheckStart) < 5*time.Minute {
|
|
statusResp, err := cli.Status(ctx)
|
|
if err == nil && statusResp != nil && statusResp.ClusterInfo != nil {
|
|
// Check if cluster itself is healthy
|
|
if statusResp.ClusterInfo.Healthy && len(statusResp.ClusterInfo.NodeInfos) == 21 {
|
|
healthy = true
|
|
break
|
|
}
|
|
ux.Logger.PrintToUser("Waiting for cluster to become healthy... (%d nodes)", len(statusResp.ClusterInfo.NodeInfos))
|
|
}
|
|
time.Sleep(5 * time.Second)
|
|
}
|
|
|
|
if !healthy {
|
|
return fmt.Errorf("network failed to become healthy after 5 minutes")
|
|
}
|
|
|
|
// Copy historic chaindata to first validator
|
|
if _, err := os.Stat(historicChainData); err == nil && len(startResp.ClusterInfo.NodeNames) > 0 {
|
|
firstNodeName := startResp.ClusterInfo.NodeNames[0]
|
|
if firstNodeInfo, ok := startResp.ClusterInfo.NodeInfos[firstNodeName]; ok && firstNodeInfo != nil {
|
|
firstNodeDataDir := firstNodeInfo.DbDir
|
|
targetCChainDir := path.Join(firstNodeDataDir, "C")
|
|
|
|
if _, err := os.Stat(targetCChainDir); os.IsNotExist(err) {
|
|
ux.Logger.PrintToUser("Copying historic C-Chain data to first validator...")
|
|
cmd := exec.Command("cp", "-r", historicChainData, targetCChainDir)
|
|
if err := cmd.Run(); err != nil {
|
|
ux.Logger.PrintToUser("Warning: Failed to copy historic chaindata: %v", err)
|
|
} else {
|
|
ux.Logger.PrintToUser("Historic chaindata copied successfully")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Display endpoints
|
|
ux.Logger.PrintToUser("\nMainnet started successfully with 21 validators!")
|
|
ux.Logger.PrintToUser("\nRPC Endpoints:")
|
|
|
|
if startResp.ClusterInfo != nil && len(startResp.ClusterInfo.NodeNames) > 0 {
|
|
for i, nodeName := range startResp.ClusterInfo.NodeNames {
|
|
if nodeInfo, ok := startResp.ClusterInfo.NodeInfos[nodeName]; ok && nodeInfo != nil && nodeInfo.Uri != "" {
|
|
ux.Logger.PrintToUser(" Validator %d: %s", i+1, nodeInfo.Uri)
|
|
}
|
|
}
|
|
|
|
// Get first node's URI
|
|
if firstNodeInfo, ok := startResp.ClusterInfo.NodeInfos[startResp.ClusterInfo.NodeNames[0]]; ok && firstNodeInfo != nil {
|
|
ux.Logger.PrintToUser("\nPrimary RPC endpoint: %s", firstNodeInfo.Uri)
|
|
}
|
|
}
|
|
|
|
ux.Logger.PrintToUser("\nData directory: %s", rootDataDir)
|
|
ux.Logger.PrintToUser("Network is ready for use!")
|
|
|
|
return nil
|
|
}
|
|
|
|
// StartTestnet starts a testnet network with 11 validator nodes
|
|
func StartTestnet() error {
|
|
ux.Logger.PrintToUser("Starting Lux testnet with 11 validator nodes...")
|
|
|
|
// First, ensure we have validator keys generated
|
|
keysDir := "/home/z/.luxd/keys/testnet"
|
|
if _, err := os.Stat(keysDir + "/validator-0/staking.crt"); os.IsNotExist(err) {
|
|
ux.Logger.PrintToUser("Generating validator keys for testnet...")
|
|
genkeysCmd := exec.Command("/home/z/.luxd/genkeys", "testnet")
|
|
genkeysCmd.Stdout = os.Stdout
|
|
genkeysCmd.Stderr = os.Stderr
|
|
if err := genkeysCmd.Run(); err != nil {
|
|
return fmt.Errorf("failed to generate keys: %w", err)
|
|
}
|
|
}
|
|
|
|
ux.Logger.PrintToUser("Using keys from %s", keysDir)
|
|
|
|
// Build the command to start luxd with testnet configuration
|
|
luxdPath := "/home/z/work/lux/node/bin/luxd"
|
|
|
|
// Check if luxd exists
|
|
if _, err := os.Stat(luxdPath); os.IsNotExist(err) {
|
|
return fmt.Errorf("luxd binary not found at %s. Please build it first with 'make build-node'", luxdPath)
|
|
}
|
|
|
|
// Testnet configuration (faster parameters)
|
|
args := []string{
|
|
"--network-id=96368",
|
|
"--staking-enabled=true",
|
|
"--consensus-x-enabled=false",
|
|
"--consensus-shutdown-timeout=3s",
|
|
"--consensus-gossip-frequency=5s",
|
|
"--consensus-k=11",
|
|
"--consensus-alpha-preference=7",
|
|
"--consensus-alpha-confidence=9",
|
|
"--consensus-beta=5",
|
|
"--consensus-concurrent-repolls=5",
|
|
"--consensus-optimal-processing=5",
|
|
"--consensus-max-processing-time=6300000000", // 6.3s in nanoseconds
|
|
"--staking-tls-cert-file=/home/z/.luxd/keys/testnet/validator-0/staking.crt",
|
|
"--staking-tls-key-file=/home/z/.luxd/keys/testnet/validator-0/staking.key",
|
|
"--staking-signer-key-file=/home/z/.luxd/keys/testnet/validator-0/signer.key",
|
|
"--genesis=/home/z/work/lux/genesis/configs/testnet",
|
|
"--chain-data-dir=/home/z/.luxd/testnet-data",
|
|
"--db-dir=/home/z/.luxd/testnet-db",
|
|
"--log-dir=/home/z/.luxd/testnet-logs",
|
|
"--http-port=9650",
|
|
"--staking-port=9651",
|
|
}
|
|
|
|
// Execute luxd
|
|
cmd := exec.Command(luxdPath, args...)
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
|
|
ux.Logger.PrintToUser("Starting luxd with command: %s %v", luxdPath, args)
|
|
|
|
if err := cmd.Start(); err != nil {
|
|
return fmt.Errorf("failed to start luxd: %w", err)
|
|
}
|
|
|
|
ux.Logger.PrintToUser("Testnet started successfully!")
|
|
ux.Logger.PrintToUser("RPC endpoint: http://localhost:9650")
|
|
ux.Logger.PrintToUser("Node logs: ~/.luxd/testnet-logs/")
|
|
return nil
|
|
}
|