Files
zeekayandHanzo Dev 59a9044b19 Record a network as soon as its nodes exist, and track every chain
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]>
2026-09-06 12:19:07 -07:00

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
}