mirror of
https://github.com/luxfi/cli.git
synced 2026-10-30 17:01:20 +00:00
Three things went wrong when a public network took a while to come up. The node config set track-chains to the string "all"; that key takes chain IDs, and the boolean that means every chain is track-all-chains, so a chain deployed after boot was not served until the nodes were restarted. The start budget was two minutes, sized for three nodes rather than for creating and bootstrapping every chain in P-chain state. And network state was written only after the network reported healthy, so a run that timed out left processes running that status, stop and clean could no longer see. State is now written as soon as the backend answers, on the timeout path as well, and the budget is ten minutes. Co-authored-by: Hanzo Dev <[email protected]>
948 lines
34 KiB
Go
948 lines
34 KiB
Go
// Copyright (C) 2022-2025, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package networkcmd
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/luxfi/cli/pkg/application"
|
|
"github.com/luxfi/cli/pkg/binutils"
|
|
"github.com/luxfi/cli/pkg/chain"
|
|
"github.com/luxfi/cli/pkg/key"
|
|
"github.com/luxfi/cli/pkg/route"
|
|
"github.com/luxfi/cli/pkg/ux"
|
|
"github.com/luxfi/constants"
|
|
"github.com/luxfi/netrunner/client"
|
|
"github.com/luxfi/netrunner/server"
|
|
"github.com/luxfi/sdk/profiles"
|
|
"github.com/spf13/cobra"
|
|
"github.com/spf13/viper"
|
|
)
|
|
|
|
var (
|
|
userProvidedLuxVersion string
|
|
snapshotName string
|
|
mainnet bool
|
|
testnet bool
|
|
devnet bool // Multi-validator devnet (port 9650)
|
|
localMode bool // 3-node localnet with light mnemonic + operator (K8s native)
|
|
devMode bool // Single-node dev mode with K=1 consensus
|
|
numValidators int
|
|
nodePath string // Path to custom luxd binary
|
|
portBase int // Base port for nodes (each node uses 2 ports)
|
|
profile string // Performance profile (standard, fast, turbo)
|
|
// BadgerDB flags
|
|
dbEngine string
|
|
archiveDir string
|
|
archiveShared bool
|
|
// K8s deployment flags
|
|
k8sCluster string // K8s cluster context name (enables K8s deployment)
|
|
k8sImage string // Docker image for K8s deployment
|
|
)
|
|
|
|
// StartFlags contains configuration for starting a network
|
|
type StartFlags struct {
|
|
UserProvidedLuxdVersion string
|
|
LuxdBinaryPath string
|
|
NumNodes uint32
|
|
}
|
|
|
|
// Start starts the local network with the given flags.
|
|
func Start(_ StartFlags, _ bool) error {
|
|
// For now, just call StartNetwork with nil cmd and args
|
|
return StartNetwork(nil, nil)
|
|
}
|
|
|
|
// profileConfig contains consensus and network tuning parameters
|
|
type profileConfig struct {
|
|
ConsensusSampleSize int
|
|
ConsensusPreferenceQuorumSize int
|
|
ConsensusConfidenceQuorumSize int
|
|
ConsensusCommitThreshold int
|
|
ConsensusConcurrentRepolls int
|
|
ConsensusOptimalProcessing int
|
|
ConsensusMaxProcessing int
|
|
ConsensusFrontierPollFreq string
|
|
HealthCheckFrequency string
|
|
HealthCheckAveragerHalflife string
|
|
NetworkMaxReconnectDelay string
|
|
NetworkInitialReconnectDelay string
|
|
NetworkInitialTimeout string
|
|
NetworkMinimumTimeout string
|
|
NetworkMaximumTimeout string
|
|
NetworkTimeoutHalflife string
|
|
NetworkReadHandshakeTimeout string
|
|
NetworkPingTimeout string
|
|
NetworkPingFrequency string
|
|
}
|
|
|
|
// getProfileConfig returns tuning parameters based on profile and network type.
|
|
// Profiles are loaded from embedded JSON files (pkg/profiles/*.json).
|
|
// Per-network defaults:
|
|
// - mainnet: standard (conservative, production-safe)
|
|
// - testnet: fast (balanced, testnet optimized)
|
|
// - devnet: turbo (aggressive, 3/5 quorum)
|
|
// - dev mode: ultra (minimal latency, single-node K=1)
|
|
func getProfileConfig(networkName, profileOverride string) profileConfig {
|
|
// Determine effective profile
|
|
effectiveProfile := profileOverride
|
|
if effectiveProfile == "" {
|
|
effectiveProfile = profiles.DefaultProfileForNetwork(networkName)
|
|
}
|
|
|
|
// Load from embedded JSON
|
|
prof, err := profiles.GetProfile(effectiveProfile)
|
|
if err != nil {
|
|
// Fall back to standard if profile not found
|
|
ux.Logger.PrintToUser("Warning: profile %q not found, using standard", effectiveProfile)
|
|
prof, _ = profiles.GetProfile("standard")
|
|
}
|
|
|
|
return profileConfig{
|
|
ConsensusSampleSize: prof.Consensus.SampleSize,
|
|
ConsensusPreferenceQuorumSize: prof.Consensus.PreferenceQuorumSize,
|
|
ConsensusConfidenceQuorumSize: prof.Consensus.ConfidenceQuorumSize,
|
|
ConsensusCommitThreshold: prof.Consensus.CommitThreshold,
|
|
ConsensusConcurrentRepolls: prof.Consensus.ConcurrentRepolls,
|
|
ConsensusOptimalProcessing: prof.Consensus.OptimalProcessing,
|
|
ConsensusMaxProcessing: prof.Consensus.MaxProcessing,
|
|
ConsensusFrontierPollFreq: prof.Consensus.FrontierPollFreq,
|
|
HealthCheckFrequency: prof.Health.CheckFrequency,
|
|
HealthCheckAveragerHalflife: prof.Health.AveragerHalflife,
|
|
NetworkMaxReconnectDelay: prof.Network.MaxReconnectDelay,
|
|
NetworkInitialReconnectDelay: prof.Network.InitialReconnectDelay,
|
|
NetworkInitialTimeout: prof.Network.InitialTimeout,
|
|
NetworkMinimumTimeout: prof.Network.MinimumTimeout,
|
|
NetworkMaximumTimeout: prof.Network.MaximumTimeout,
|
|
NetworkTimeoutHalflife: prof.Network.TimeoutHalflife,
|
|
NetworkReadHandshakeTimeout: prof.Network.ReadHandshakeTimeout,
|
|
NetworkPingTimeout: prof.Network.PingTimeout,
|
|
NetworkPingFrequency: prof.Network.PingFrequency,
|
|
}
|
|
}
|
|
|
|
const nodeBinaryName = "luxd"
|
|
|
|
// findNodeBinary locates the node binary using the following priority:
|
|
// 1. User-provided --node-path flag
|
|
// 2. ~/.lux/bin/luxd (symlinked via 'lux node link')
|
|
// 3. NODE_PATH environment variable
|
|
// 4. Config file node-path setting (~/.lux/cli.json)
|
|
// 5. Node binary in PATH
|
|
// 6. Relative to CLI binary: ../node/build/<nodeBinaryName>
|
|
func findNodeBinary() (string, error) {
|
|
// Priority 1: User-provided path via --node-path flag
|
|
if nodePath != "" {
|
|
if _, err := os.Stat(nodePath); os.IsNotExist(err) {
|
|
return "", fmt.Errorf("%s binary not found at specified path: %s", nodeBinaryName, nodePath)
|
|
}
|
|
return nodePath, nil
|
|
}
|
|
|
|
// Priority 2: Check ~/.lux/bin/luxd (symlinked binary)
|
|
home, _ := os.UserHomeDir()
|
|
linkedPath := filepath.Join(home, constants.BaseDirName, constants.BinDir, nodeBinaryName)
|
|
if info, err := os.Stat(linkedPath); err == nil && !info.IsDir() {
|
|
return linkedPath, nil
|
|
}
|
|
|
|
// Priority 3 & 4: Check viper (handles both env var and config file)
|
|
// viper automatically checks NODE_PATH env var first, then config file
|
|
if configPath := viper.GetString(constants.ConfigNodePath); configPath != "" {
|
|
// Expand ~ to home directory
|
|
if strings.HasPrefix(configPath, "~") {
|
|
configPath = filepath.Join(home, configPath[1:])
|
|
}
|
|
if _, err := os.Stat(configPath); err == nil {
|
|
return configPath, nil
|
|
}
|
|
// Config path is set but invalid - warn but continue searching
|
|
ux.Logger.PrintToUser("Warning: node-path (%s) not found, searching alternatives...", configPath)
|
|
}
|
|
|
|
// Priority 5: Check if node binary is in PATH
|
|
if binaryPath, err := exec.LookPath(nodeBinaryName); err == nil {
|
|
return binaryPath, nil
|
|
}
|
|
|
|
// Priority 6: Look relative to CLI binary location
|
|
// Get the path of the current executable
|
|
execPath, err := os.Executable()
|
|
if err == nil {
|
|
// Resolve any symlinks
|
|
execPath, err = filepath.EvalSymlinks(execPath)
|
|
if err == nil {
|
|
// CLI is typically at cli/bin/lux, so node binary would be at ../node/build/<nodeBinaryName>
|
|
cliDir := filepath.Dir(filepath.Dir(execPath)) // Go up two levels from bin/lux
|
|
relativePath := filepath.Join(cliDir, "..", "node", "build", nodeBinaryName)
|
|
if absPath, err := filepath.Abs(relativePath); err == nil {
|
|
if _, err := os.Stat(absPath); err == nil {
|
|
return absPath, nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return "", fmt.Errorf("%s binary not found. Please either:\n"+
|
|
" 1. Use --node-path flag to specify the path\n"+
|
|
" 2. Run 'lux node link' to symlink a binary to ~/.lux/bin/luxd\n"+
|
|
" 3. Set NODE_PATH environment variable\n"+
|
|
" 4. Set node-path in ~/.lux/cli.json config file\n"+
|
|
" 5. Add %s to your PATH\n"+
|
|
" 6. Build %s in the sibling node directory (../node/build/%s)",
|
|
nodeBinaryName, nodeBinaryName, nodeBinaryName, nodeBinaryName)
|
|
}
|
|
|
|
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.
|
|
|
|
NETWORK TYPES (choose one, required):
|
|
|
|
--mainnet, -m Production mainnet with 3 validators (port 9630)
|
|
- Network ID: 1
|
|
- HTTP API: ports 9630-9638
|
|
- Use for mainnet testing and development
|
|
|
|
--testnet, -t Test network with 3 validators (port 9640)
|
|
- Network ID: 2
|
|
- HTTP API: ports 9640-9648
|
|
- Use for testnet deployment testing
|
|
|
|
--devnet, -d Development network with 3 validators (port 9650)
|
|
- Network ID: 3
|
|
- HTTP API: ports 9650-9658
|
|
- Use for rapid local development
|
|
|
|
--dev Dev mode (port 8545) - Anvil/Hardhat compatible
|
|
- Single-node: K=1 consensus, instant finality
|
|
- Multi-node: --dev --num-validators=3 (turbo profile)
|
|
- Primary chains: C/P/X (Contract/Platform/Exchange)
|
|
- App chain VMs: A(AI) B(Bridge) D(DEX) G(Graph) I(Identity)
|
|
K(Key) O(Oracle) Q(Quantum) R(Relay) T(Threshold) Z(ZK)
|
|
- Set MNEMONIC to auto-fund derived accounts
|
|
|
|
OPTIONS:
|
|
|
|
--num-validators Number of validator nodes (default: 3)
|
|
With --dev: 1 = K=1 single-node, >1 = turbo multi-node
|
|
--node-path Path to custom luxd binary
|
|
--node-version luxd version to use (default: latest)
|
|
--snapshot-name Resume from named snapshot
|
|
--port Base port for APIs (overrides defaults)
|
|
--profile Consensus profile: standard, fast, turbo (default: auto)
|
|
|
|
EXAMPLES:
|
|
|
|
# Start mainnet (3 validators, port 9630)
|
|
lux network start --mainnet
|
|
lux network start -m
|
|
|
|
# Start testnet with custom validator count
|
|
lux network start --testnet --num-validators 2
|
|
|
|
# Start devnet (most common for development)
|
|
lux network start --devnet
|
|
|
|
# Start single-node dev mode for rapid testing (K=1)
|
|
lux network start --dev
|
|
|
|
# Start 3-validator dev mode with turbo consensus
|
|
lux network start --dev --num-validators=3
|
|
|
|
# 3-node dev with mnemonic-funded accounts
|
|
export LIGHT_MNEMONIC="light light light light light light light light light light light energy"
|
|
lux network start --dev --num-validators=3
|
|
|
|
# Use custom luxd binary
|
|
lux network start --devnet --node-path ~/work/lux/node/build/luxd
|
|
|
|
NOTES:
|
|
|
|
- Only one network type can run at a time
|
|
- Each network type uses different ports to avoid conflicts
|
|
- Network data is stored in ~/.lux/networks/<type>
|
|
- Use 'lux network status' to verify the network is running
|
|
- Use 'lux network stop' to stop and save a snapshot
|
|
- Admin APIs are enabled by default for chain deployment
|
|
|
|
TYPICAL WORKFLOW:
|
|
|
|
1. Start network: lux network start --devnet
|
|
2. Deploy chain: lux chain deploy mychain
|
|
3. Test your dapp: (connect to ` + route.Chain("http://localhost:9650", "C") + `/rpc)
|
|
4. Stop network: lux network stop`,
|
|
|
|
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(&nodePath, "node-path", "", "path to local luxd binary (overrides --node-version)")
|
|
cmd.Flags().StringVar(&snapshotName, "snapshot-name", constants.DefaultSnapshotName, "name of snapshot to use to start the network from")
|
|
cmd.Flags().BoolVarP(&mainnet, "mainnet", "m", false, "start mainnet with 3 validators (port 9630)")
|
|
cmd.Flags().BoolVarP(&testnet, "testnet", "t", false, "start testnet with 3 validators (port 9640)")
|
|
cmd.Flags().BoolVarP(&devnet, "devnet", "d", false, "start devnet with 3 validators (port 9650)")
|
|
cmd.Flags().BoolVarP(&localMode, "local", "l", false, "start 3-node localnet on K8s (operator-native, light mnemonic)")
|
|
cmd.Flags().BoolVar(&devMode, "dev", false, "single-node dev mode with K=1 consensus")
|
|
cmd.Flags().IntVar(&numValidators, "num-validators", constants.LocalNetworkNumNodes, "number of validators to start")
|
|
cmd.Flags().IntVar(&portBase, "port", 9630, "base port for node APIs (each node uses 2 ports: HTTP and staking)")
|
|
cmd.Flags().StringVar(&profile, "profile", "", "performance profile: standard, fast, turbo (default: per-network)")
|
|
// 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")
|
|
|
|
// Add state loading flags
|
|
AddStateFlags(cmd)
|
|
|
|
// K8s deployment flags
|
|
cmd.Flags().StringVar(&k8sCluster, "k8s", "", "deploy to Kubernetes cluster (use kubeconfig context name)")
|
|
cmd.Flags().StringVar(&k8sImage, "k8s-image", "ghcr.io/luxfi/node:latest", "Docker image for K8s deployment")
|
|
|
|
return cmd
|
|
}
|
|
|
|
// StartNetwork starts the local network.
|
|
func StartNetwork(*cobra.Command, []string) error {
|
|
// Check for conflicting flags
|
|
flagCount := 0
|
|
if mainnet {
|
|
flagCount++
|
|
}
|
|
if testnet {
|
|
flagCount++
|
|
}
|
|
if devnet {
|
|
flagCount++
|
|
}
|
|
if localMode {
|
|
flagCount++
|
|
}
|
|
if devMode {
|
|
flagCount++
|
|
}
|
|
if flagCount > 1 {
|
|
return fmt.Errorf("cannot use multiple network flags together (--mainnet, --testnet, --devnet, --local, --dev)")
|
|
}
|
|
|
|
// --local: K8s operator-native localnet (no netrunner)
|
|
if localMode {
|
|
return StartLocal()
|
|
}
|
|
|
|
// K8s deployment mode (netrunner not used ā operator manages everything)
|
|
if k8sCluster != "" {
|
|
if devMode {
|
|
return fmt.Errorf("--dev mode is not supported with --k8s, use --devnet instead")
|
|
}
|
|
if mainnet {
|
|
return StartK8sMainnet()
|
|
}
|
|
if testnet {
|
|
return StartK8sTestnet()
|
|
}
|
|
if devnet {
|
|
return StartK8sDevnet()
|
|
}
|
|
return fmt.Errorf("please specify --mainnet, --testnet, or --devnet with --k8s")
|
|
}
|
|
|
|
// Dev mode - single node or multi-node development network
|
|
if devMode {
|
|
// If num-validators > 1, use multi-node dev network with turbo profile
|
|
if numValidators > 1 {
|
|
return StartDevNetwork()
|
|
}
|
|
// Single node dev mode with K=1 consensus
|
|
return StartDevMode()
|
|
}
|
|
|
|
// If mainnet, testnet, or devnet flag is set, delegate to the appropriate function
|
|
if mainnet {
|
|
return StartMainnet()
|
|
}
|
|
if testnet {
|
|
return StartTestnet()
|
|
}
|
|
if devnet {
|
|
return StartDevnet()
|
|
}
|
|
// No network flag specified - require explicit network type
|
|
return fmt.Errorf("please specify --mainnet, --testnet, --devnet, or --dev")
|
|
}
|
|
|
|
// networkConfig holds configuration for starting a public network
|
|
type networkConfig struct {
|
|
networkID uint32
|
|
networkName string // "mainnet" or "testnet"
|
|
portBase int // Base port for APIs (defaults to 9630 for mainnet, 9640 for testnet)
|
|
}
|
|
|
|
// startPublicNetwork handles the common logic for starting mainnet/testnet
|
|
func startPublicNetwork(cfg networkConfig) error {
|
|
if numValidators < 1 {
|
|
numValidators = constants.LocalNetworkNumNodes
|
|
}
|
|
ux.Logger.PrintToUser("Starting Lux %s with %d validator nodes...", cfg.networkName, numValidators)
|
|
ux.Logger.PrintToUser("Network ID: %d", cfg.networkID)
|
|
|
|
localNodePath, err := findNodeBinary()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Create deployer for the specific network type
|
|
sd := chain.NewLocalDeployerForNetwork(app, "", "", cfg.networkName)
|
|
if err := sd.StartServerForNetwork(cfg.networkName); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Connect to this network's gRPC server
|
|
cli, err := binutils.NewGRPCClient(binutils.WithNetworkType(cfg.networkName))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() { _ = cli.Close() }()
|
|
|
|
// Track every chain in P-chain state, so a chain deployed later comes up
|
|
// without restarting the nodes. luxd reads this as the track-all-chains
|
|
// bool; track-chains takes chain IDs and would pin the set at boot.
|
|
netIDs, trackErr := chain.GetLocallyDeployedNetIDs(app)
|
|
if trackErr == nil && len(netIDs) > 0 {
|
|
ux.Logger.PrintToUser("Tracking all chains, including %d already deployed: %s",
|
|
len(netIDs), strings.Join(netIDs, ","))
|
|
}
|
|
|
|
// Use port base from config, default 9630 for mainnet, 9640 for testnet
|
|
effectivePortBase := cfg.portBase
|
|
if effectivePortBase == 0 {
|
|
effectivePortBase = 9630
|
|
}
|
|
|
|
// Get profile-specific tuning parameters
|
|
// Per-network defaults: mainnet=standard, testnet=fast, devnet=turbo
|
|
prof := getProfileConfig(cfg.networkName, profile)
|
|
ux.Logger.PrintToUser("Using profile: %s (override: %q)", cfg.networkName, profile)
|
|
|
|
// Build node config using profile parameters
|
|
importChainDataConfig := ""
|
|
if importChainData != "" {
|
|
importChainDataConfig = fmt.Sprintf(`"import-chain-data": %q,`, importChainData)
|
|
}
|
|
|
|
globalNodeConfig := fmt.Sprintf(`{
|
|
"network-id": %d,
|
|
%s
|
|
"db-type": "badgerdb",
|
|
"sybil-protection-enabled": true,
|
|
"network-allow-private-ips": true,
|
|
"http-host": "127.0.0.1",
|
|
"api-admin-enabled": true,
|
|
"enable-automining": true,
|
|
"index-enabled": true,
|
|
"track-all-chains": true,
|
|
"log-level": "error",
|
|
"log-display-level": "error",
|
|
|
|
"consensus-frontier-poll-frequency": %q,
|
|
"consensus-shutdown-timeout": "2s",
|
|
"consensus-app-concurrency": 64,
|
|
|
|
"health-check-frequency": %q,
|
|
"health-check-averager-halflife": %q,
|
|
|
|
"bootstrap-beacon-connection-timeout": "1s",
|
|
"bootstrap-max-time-get-ancestors": "25ms",
|
|
|
|
"network-peer-list-pull-gossip-frequency": "250ms",
|
|
"network-peer-list-bloom-reset-frequency": "30s",
|
|
"network-max-reconnect-delay": %q,
|
|
"network-initial-reconnect-delay": %q,
|
|
"network-initial-timeout": %q,
|
|
"network-minimum-timeout": %q,
|
|
"network-maximum-timeout": %q,
|
|
"network-timeout-halflife": %q,
|
|
"network-read-handshake-timeout": %q,
|
|
"network-ping-timeout": %q,
|
|
"network-ping-frequency": %q,
|
|
"network-health-max-time-since-msg-sent": "5s",
|
|
"network-health-max-time-since-msg-received": "5s",
|
|
"network-outbound-connection-timeout": "500ms"
|
|
}`, cfg.networkID, importChainDataConfig,
|
|
prof.ConsensusFrontierPollFreq,
|
|
prof.HealthCheckFrequency,
|
|
prof.HealthCheckAveragerHalflife,
|
|
prof.NetworkMaxReconnectDelay,
|
|
prof.NetworkInitialReconnectDelay,
|
|
prof.NetworkInitialTimeout,
|
|
prof.NetworkMinimumTimeout,
|
|
prof.NetworkMaximumTimeout,
|
|
prof.NetworkTimeoutHalflife,
|
|
prof.NetworkReadHandshakeTimeout,
|
|
prof.NetworkPingTimeout,
|
|
prof.NetworkPingFrequency)
|
|
|
|
// Build per-node configs with explicit ports to avoid conflicts
|
|
customNodeConfigs := make(map[string]string)
|
|
for i := 0; i < numValidators; i++ {
|
|
nodeName := fmt.Sprintf("node%d", i+1)
|
|
httpPort := effectivePortBase + (i * 2)
|
|
stakingPort := httpPort + 1
|
|
customNodeConfigs[nodeName] = fmt.Sprintf(`{"http-port": %d, "staking-port": %d}`, httpPort, stakingPort)
|
|
}
|
|
|
|
rootDataDir, err := chain.EnsureNetworkRunDir(app.GetRunDir(), cfg.networkName)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to ensure %s run directory: %w", cfg.networkName, err)
|
|
}
|
|
|
|
// Check for existing data or user-provided state
|
|
if statePath != "" {
|
|
ux.Logger.PrintToUser("Resuming from user-provided state: %s", statePath)
|
|
} else {
|
|
entries, _ := os.ReadDir(rootDataDir)
|
|
for _, e := range entries {
|
|
if e.IsDir() && strings.HasPrefix(e.Name(), "node") {
|
|
ux.Logger.PrintToUser("Resuming from existing %s data: %s", cfg.networkName, rootDataDir)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
opts := []client.OpOption{
|
|
client.WithNumNodes(uint32(numValidators)), //nolint:gosec // G115: numValidators is bounded (1-3)
|
|
client.WithGlobalNodeConfig(globalNodeConfig),
|
|
client.WithRootDataDir(rootDataDir),
|
|
client.WithReassignPortsIfUsed(true),
|
|
client.WithDynamicPorts(false),
|
|
client.WithCustomNodeConfigs(customNodeConfigs),
|
|
}
|
|
|
|
// Build chain configs (mainnet-specific feature, but harmless for testnet)
|
|
cfgMgr := chain.NewManager(app)
|
|
if err := cfgMgr.LoadDeployedChains(); err != nil {
|
|
ux.Logger.PrintToUser("Warning: failed to load deployed chains: %v", err)
|
|
}
|
|
cfgMgr.EnableAdminAll()
|
|
|
|
for chainID, cfg := range cfgMgr.ToNetrunnerMap() {
|
|
if chainID != "C" {
|
|
ux.Logger.PrintToUser("Configured chain: %s (admin API enabled)", chainID[:min(len(chainID), 12)])
|
|
_ = cfg
|
|
}
|
|
}
|
|
opts = append(opts, client.WithChainConfigs(cfgMgr.ToNetrunnerMap()))
|
|
|
|
pluginDir := filepath.Join(app.GetPluginsDir(), "current")
|
|
// Always ensure plugin dir exists and pass it to nodes
|
|
if err := os.MkdirAll(pluginDir, 0o750); err != nil {
|
|
return fmt.Errorf("failed to create plugin directory %s: %w", pluginDir, err)
|
|
}
|
|
opts = append(opts, client.WithPluginDir(pluginDir))
|
|
|
|
// Every chain in P-chain state is created and bootstrapped before the
|
|
// network reports healthy, so this budget scales with the number of
|
|
// chains, not just the three nodes. Ten minutes is a ceiling that still
|
|
// fails rather than hanging; a local run reaches healthy well inside it.
|
|
startCtx, startCancel := context.WithTimeout(context.Background(), 10*time.Minute)
|
|
defer startCancel()
|
|
|
|
ux.Logger.PrintToUser("Starting network with genesis from luxfi/genesis package...")
|
|
ux.Logger.PrintToUser("Using luxd binary: %s", localNodePath)
|
|
ux.Logger.PrintToUser("Root data directory: %s", rootDataDir)
|
|
|
|
startResp, err := cli.Start(startCtx, localNodePath, opts...)
|
|
if err != nil {
|
|
// Check if network is already bootstrapped (backend was started previously)
|
|
errStr := err.Error()
|
|
if !server.IsServerError(err, server.ErrAlreadyBootstrapped) && !strings.Contains(errStr, "already bootstrapped") {
|
|
// The backend and its nodes can be running even when this call
|
|
// does not return in time, so record them before giving up.
|
|
saveNetworkState(cfg, effectivePortBase)
|
|
return fmt.Errorf("failed to start network: %w", err)
|
|
}
|
|
ux.Logger.PrintToUser("Network has already been started. Continuing with existing network...")
|
|
}
|
|
saveNetworkState(cfg, effectivePortBase)
|
|
|
|
ux.Logger.PrintToUser("Waiting for all validators to become healthy...")
|
|
clusterInfo, err := chain.WaitForHealthy(startCtx, cli)
|
|
if err != nil {
|
|
return fmt.Errorf("failed waiting for network to become healthy: %w", err)
|
|
}
|
|
|
|
// Capitalize first letter of network name
|
|
displayName := strings.ToUpper(cfg.networkName[:1]) + cfg.networkName[1:]
|
|
ux.Logger.PrintToUser("\n%s started successfully with %d validators!", displayName, numValidators)
|
|
ux.Logger.PrintToUser("\nRPC Endpoints:")
|
|
|
|
if startResp != nil && 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)
|
|
}
|
|
}
|
|
}
|
|
|
|
if chain.HasEndpoints(clusterInfo) {
|
|
ux.PrintTableEndpoints(clusterInfo)
|
|
}
|
|
|
|
// Print all native chain RPC endpoints
|
|
ux.PrintCompactChainEndpoints(effectivePortBase)
|
|
|
|
// Display validator keys from configured sources
|
|
displayValidatorKeys(cfg.networkID, numValidators)
|
|
|
|
ux.Logger.PrintToUser("\nš Data directory: %s", rootDataDir)
|
|
ux.Logger.PrintToUser("ā
Network is ready for use!")
|
|
|
|
ux.Logger.PrintToUser("gRPC server: localhost:%d", binutils.GetGRPCPorts(cfg.networkName).Server)
|
|
|
|
return nil
|
|
}
|
|
|
|
// saveNetworkState records what was started so status, send, stop and clean
|
|
// can find it. Called as soon as the backend is up rather than after the
|
|
// network reports healthy: nodes that are still bootstrapping are running
|
|
// processes, and a network nothing is tracking cannot be stopped.
|
|
func saveNetworkState(cfg networkConfig, portBase int) {
|
|
grpcPorts := binutils.GetGRPCPorts(cfg.networkName)
|
|
networkState := application.CreateNetworkStateWithGRPC(cfg.networkName, cfg.networkID, portBase, grpcPorts.Server, grpcPorts.Gateway)
|
|
|
|
validators := deriveValidatorAddresses(cfg.networkID, numValidators)
|
|
networkState.Validators = validators
|
|
if len(validators) > 0 {
|
|
networkState.ActiveAccount = &application.ActiveAccountInfo{
|
|
Index: validators[0].Index,
|
|
PChainAddress: validators[0].PChainAddress,
|
|
XChainAddress: validators[0].XChainAddress,
|
|
CChainAddress: validators[0].CChainAddress,
|
|
}
|
|
}
|
|
|
|
if err := app.SaveNetworkState(networkState); err != nil {
|
|
ux.Logger.PrintToUser("Warning: failed to save network state: %v", err)
|
|
}
|
|
}
|
|
|
|
// StartMainnet starts a mainnet network with configurable validator nodes
|
|
func StartMainnet() error {
|
|
// Use --port-base flag if provided, otherwise default to 9630
|
|
pb := portBase
|
|
if pb == 9630 && !isPortBaseFlagSet() {
|
|
pb = 9630 // mainnet default
|
|
}
|
|
return startPublicNetwork(networkConfig{
|
|
networkID: constants.MainnetID, // P-Chain network ID (1)
|
|
networkName: "mainnet",
|
|
portBase: pb,
|
|
})
|
|
}
|
|
|
|
// StartTestnet starts a testnet network with configurable validator nodes
|
|
func StartTestnet() error {
|
|
// Use --port-base flag if provided, otherwise default to 9640
|
|
pb := portBase
|
|
if pb == 9630 && !isPortBaseFlagSet() {
|
|
pb = 9640 // testnet default (separate from mainnet)
|
|
}
|
|
return startPublicNetwork(networkConfig{
|
|
networkID: constants.TestnetID, // P-Chain network ID (2)
|
|
networkName: "testnet",
|
|
portBase: pb,
|
|
})
|
|
}
|
|
|
|
// StartDevnet starts a devnet network with configurable validator nodes
|
|
func StartDevnet() error {
|
|
// Use --port-base flag if provided, otherwise default to 9650
|
|
pb := portBase
|
|
if pb == 9630 && !isPortBaseFlagSet() {
|
|
pb = 9650 // devnet default (separate from mainnet/testnet)
|
|
}
|
|
return startPublicNetwork(networkConfig{
|
|
networkID: constants.DevnetID, // P-Chain network ID (3)
|
|
networkName: "devnet",
|
|
portBase: pb,
|
|
})
|
|
}
|
|
|
|
// StartDevNetwork starts a multi-node development network with turbo profile
|
|
// This is the hybrid mode: multiple validators with fast consensus (K=3, 2/3 quorum)
|
|
// Use this when you need to test multi-validator scenarios with fast finality
|
|
// The network uses MNEMONIC to fund derived accounts automatically
|
|
func StartDevNetwork() error {
|
|
ux.Logger.PrintToUser("Starting Lux dev network (%d validators, turbo consensus)...", numValidators)
|
|
|
|
// Use dev mode port base (8545) if not explicitly set
|
|
pb := portBase
|
|
if pb == 9630 && !isPortBaseFlagSet() {
|
|
pb = 8545 // anvil/hardhat compatible for dev tooling
|
|
}
|
|
|
|
// Force turbo profile for multi-node dev (K=3, 2/3 quorum for 3 nodes)
|
|
// ultra profile is only for single-node K=1 mode
|
|
if profile == "" {
|
|
profile = "turbo"
|
|
}
|
|
|
|
// Display mnemonic-derived addresses that will be funded
|
|
mnemonic := key.GetMnemonicFromEnv()
|
|
if mnemonic != "" {
|
|
ux.Logger.PrintToUser("\nš° Funded Accounts (derived from LIGHT_MNEMONIC):")
|
|
for i := 0; i < numValidators; i++ {
|
|
sf, err := key.NewSoftFromMnemonicWithAccount(1337, mnemonic, uint32(i))
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if i == 0 {
|
|
ux.Logger.PrintToUser(" [%d] %s (primary)", i, sf.C())
|
|
} else {
|
|
ux.Logger.PrintToUser(" [%d] %s", i, sf.C())
|
|
}
|
|
}
|
|
ux.Logger.PrintToUser("")
|
|
} else {
|
|
ux.Logger.PrintToUser("\nš” Tip: Set LIGHT_MNEMONIC to fund derived accounts")
|
|
ux.Logger.PrintToUser(" Example: export LIGHT_MNEMONIC=\"light light light light light light light light light light light energy\"")
|
|
ux.Logger.PrintToUser("")
|
|
}
|
|
|
|
// Start the network using dev-mode configuration
|
|
// This uses the turbo profile for fast consensus but with multiple validators
|
|
return startPublicNetwork(networkConfig{
|
|
networkID: 1337, // Dev network ID
|
|
networkName: "dev",
|
|
portBase: pb,
|
|
})
|
|
}
|
|
|
|
// StartDevMode starts a single-node development network with K=1 consensus
|
|
// This runs luxd directly (not through netrunner) for maximum simplicity
|
|
// luxd's built-in --dev flag enables: single-node consensus, no sybil protection, instant blocks
|
|
func StartDevMode() error {
|
|
ux.Logger.PrintToUser("Starting Lux dev mode (single node, K=1 consensus)...")
|
|
ux.Logger.PrintToUser("All chains enabled: C-Chain, P-Chain, X-Chain")
|
|
|
|
localNodePath, err := findNodeBinary()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Dev mode uses port 8545 by default (anvil/hardhat compatible)
|
|
effectivePortBase := portBase
|
|
if effectivePortBase == 9630 && !isPortBaseFlagSet() {
|
|
effectivePortBase = 8545 // anvil/hardhat default for dev tooling compatibility
|
|
}
|
|
|
|
// Set up data directory
|
|
dataDir := filepath.Join(os.Getenv("HOME"), ".lux", "devnet")
|
|
dbDir := filepath.Join(dataDir, "db")
|
|
logDir := filepath.Join(dataDir, "logs")
|
|
|
|
// Clean up stale database to prevent genesis hash mismatch
|
|
if _, err := os.Stat(dbDir); err == nil {
|
|
ux.Logger.PrintToUser("Cleaning stale dev database...")
|
|
if err := os.RemoveAll(dbDir); err != nil {
|
|
ux.Logger.PrintToUser("Warning: failed to clean dev database: %v", err)
|
|
}
|
|
}
|
|
|
|
// Ensure directories exist
|
|
if err := os.MkdirAll(logDir, 0o750); err != nil {
|
|
return fmt.Errorf("failed to create log directory: %w", err)
|
|
}
|
|
|
|
ux.Logger.PrintToUser("Using luxd binary: %s", localNodePath)
|
|
ux.Logger.PrintToUser("Data directory: %s", dataDir)
|
|
|
|
// luxd has no `--dev` shortcut; spell out the K=1, no-bootstrap,
|
|
// no-sybil-protection profile explicitly. --automine supplies
|
|
// single-validator-quorum consensus and instant finality.
|
|
args := []string{
|
|
"--automine",
|
|
"--consensus-sample-size=1",
|
|
"--consensus-quorum-size=1",
|
|
"--sybil-protection-enabled=false",
|
|
"--skip-bootstrap=true",
|
|
fmt.Sprintf("--network-id=%d", 1337),
|
|
fmt.Sprintf("--http-host=%s", "0.0.0.0"),
|
|
fmt.Sprintf("--http-port=%d", effectivePortBase),
|
|
fmt.Sprintf("--staking-port=%d", effectivePortBase+1),
|
|
fmt.Sprintf("--data-dir=%s", dataDir),
|
|
fmt.Sprintf("--log-dir=%s", logDir),
|
|
"--log-level=info",
|
|
"--api-admin-enabled=true",
|
|
"--index-enabled=true",
|
|
"--db-type=badgerdb",
|
|
}
|
|
|
|
cmd := exec.Command(localNodePath, args...) //nolint:gosec // G204: Running our own luxd binary
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
|
|
// Start the node in the background
|
|
if err := cmd.Start(); err != nil {
|
|
return fmt.Errorf("failed to start luxd: %w", err)
|
|
}
|
|
|
|
ux.Logger.PrintToUser("luxd started (PID: %d)", cmd.Process.Pid)
|
|
ux.Logger.PrintToUser("Waiting for node to become healthy...")
|
|
|
|
// Wait for health endpoint to respond with explicit timeout
|
|
healthURL := route.Readiness(fmt.Sprintf("http://localhost:%d", effectivePortBase))
|
|
healthTimeout := 90 * time.Second // Dev mode can take longer to initialize all chains
|
|
healthCtx, healthCancel := context.WithTimeout(context.Background(), healthTimeout)
|
|
defer healthCancel()
|
|
|
|
ticker := time.NewTicker(1 * time.Second)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-healthCtx.Done():
|
|
return fmt.Errorf("timeout waiting for node to become healthy after %s: %w", healthTimeout, healthCtx.Err())
|
|
case <-ticker.C:
|
|
resp, err := http.Get(healthURL)
|
|
if err != nil {
|
|
continue // Network not ready yet
|
|
}
|
|
_ = resp.Body.Close()
|
|
if resp.StatusCode == 200 {
|
|
goto healthy
|
|
}
|
|
}
|
|
}
|
|
healthy:
|
|
|
|
ux.Logger.PrintToUser("\nš Dev mode started successfully!")
|
|
|
|
// Print all native chain RPC endpoints
|
|
ux.PrintCompactChainEndpoints(effectivePortBase)
|
|
ux.Logger.PrintToUser("\nā” Features:")
|
|
ux.Logger.PrintToUser(" ⢠K=1 consensus (instant blocks)")
|
|
ux.Logger.PrintToUser(" ⢠POA single-node mode")
|
|
ux.Logger.PrintToUser(" ⢠No validator sampling")
|
|
ux.Logger.PrintToUser(" ⢠Auto-mining enabled")
|
|
ux.Logger.PrintToUser(" ⢠Full API access (admin, IPC, index)")
|
|
ux.Logger.PrintToUser("\nData directory: %s", dataDir)
|
|
ux.Logger.PrintToUser("Logs: %s", logDir)
|
|
ux.Logger.PrintToUser("\nDev network is ready for use!")
|
|
ux.Logger.PrintToUser("To stop: pkill luxd")
|
|
|
|
// Wait for the node process to keep running
|
|
return cmd.Wait()
|
|
}
|
|
|
|
// isPortBaseFlagSet checks if --port-base was explicitly set by user
|
|
func isPortBaseFlagSet() bool {
|
|
// If portBase != default, it was explicitly set
|
|
return portBase != 9630
|
|
}
|
|
|
|
// displayValidatorKeys displays validator keys from configured sources
|
|
// Priority: MNEMONIC > PRIVATE_KEY > ~/.lux/keys/
|
|
func displayValidatorKeys(networkID uint32, numValidators int) {
|
|
var validators []ux.ValidatorKeyInfo
|
|
|
|
// Check for mnemonic-based derivation first (allows deriving N validators)
|
|
if mnemonic := key.GetMnemonicFromEnv(); mnemonic != "" {
|
|
ux.Logger.PrintToUser("\nš Validator Keys (derived from MNEMONIC):")
|
|
for i := 0; i < numValidators; i++ {
|
|
keySet, err := key.DeriveAllKeysWithAccount(fmt.Sprintf("validator%d", i+1), mnemonic, uint32(i))
|
|
if err != nil {
|
|
continue
|
|
}
|
|
sf, err := key.NewSoftFromMnemonicWithAccount(networkID, mnemonic, uint32(i))
|
|
if err != nil {
|
|
continue
|
|
}
|
|
validators = append(validators, ux.ValidatorKeyInfo{
|
|
Index: i + 1,
|
|
NodeID: keySet.NodeID,
|
|
PChainAddr: sf.P()[0],
|
|
XChainAddr: sf.X()[0],
|
|
CChainAddr: sf.C(),
|
|
})
|
|
}
|
|
} else if privKey := os.Getenv("PRIVATE_KEY"); privKey != "" {
|
|
// Single key from PRIVATE_KEY
|
|
ux.Logger.PrintToUser("\nš Key (from PRIVATE_KEY):")
|
|
sf, err := key.NewSoft(networkID, key.WithPrivateKeyEncoded(privKey))
|
|
if err == nil {
|
|
validators = append(validators, ux.ValidatorKeyInfo{
|
|
Index: 1,
|
|
PChainAddr: sf.P()[0],
|
|
XChainAddr: sf.X()[0],
|
|
CChainAddr: sf.C(),
|
|
})
|
|
}
|
|
} else {
|
|
// Show tip about setting keys
|
|
ux.Logger.PrintToUser("\nš” Tip: Set MNEMONIC to display validator keys for testing")
|
|
return // No validators to display
|
|
}
|
|
|
|
// Print validators if any were derived
|
|
for _, v := range validators {
|
|
if v.NodeID != "" {
|
|
ux.Logger.PrintToUser(" [%d] %s | C: %s", v.Index, v.NodeID, v.CChainAddr)
|
|
} else {
|
|
ux.Logger.PrintToUser(" [%d] P: %s | C: %s", v.Index, v.PChainAddr, v.CChainAddr)
|
|
}
|
|
}
|
|
}
|
|
|
|
// deriveValidatorAddresses derives and returns validator addresses for storing in network state
|
|
// Priority: MNEMONIC > PRIVATE_KEY
|
|
func deriveValidatorAddresses(networkID uint32, numValidators int) []application.ValidatorInfo {
|
|
var validators []application.ValidatorInfo
|
|
|
|
// Check for mnemonic-based derivation first (allows deriving N validators)
|
|
if mnemonic := key.GetMnemonicFromEnv(); mnemonic != "" {
|
|
for i := 0; i < numValidators; i++ {
|
|
keySet, err := key.DeriveAllKeysWithAccount(fmt.Sprintf("validator%d", i+1), mnemonic, uint32(i))
|
|
if err != nil {
|
|
continue
|
|
}
|
|
sf, err := key.NewSoftFromMnemonicWithAccount(networkID, mnemonic, uint32(i))
|
|
if err != nil {
|
|
continue
|
|
}
|
|
validators = append(validators, application.ValidatorInfo{
|
|
Index: i + 1,
|
|
NodeID: keySet.NodeID,
|
|
PChainAddress: sf.P()[0],
|
|
XChainAddress: sf.X()[0],
|
|
CChainAddress: sf.C(),
|
|
})
|
|
}
|
|
} else if privKey := os.Getenv("PRIVATE_KEY"); privKey != "" {
|
|
// Single key from PRIVATE_KEY
|
|
sf, err := key.NewSoft(networkID, key.WithPrivateKeyEncoded(privKey))
|
|
if err == nil {
|
|
validators = append(validators, application.ValidatorInfo{
|
|
Index: 1,
|
|
PChainAddress: sf.P()[0],
|
|
XChainAddress: sf.X()[0],
|
|
CChainAddress: sf.C(),
|
|
})
|
|
}
|
|
}
|
|
|
|
return validators
|
|
}
|