Files
zeekayandHanzo Dev 62d5f28c22 A chain address is built in one place
The node moved every chain route from /v1/bc/<alias> to /v1/chain/<alias>
and deleted the old prefix, so every chain call answered 404. The address
was spelled out at 112 sites across 34 files and constants.ChainAliasPrefix
was used at none of them, which is why nothing failed to compile when the
segment moved.

route.Chain(uri, alias) composes it now, and route.Alias reads one back,
replacing three hand-rolled parsers — one of which took the alias by
position in the split path and only worked because the new segment happens
to leave the component count unchanged.

TestChainAddressBuiltOnlyHere walks every .go file in the module and fails
on a chain address in a string literal, so the segment cannot drift back;
prose is free to name a route. TestGuardCatchesEachShape plants each
forbidden shape and requires the guard to find it.

The endpoint box and the eleven-chain table were the same list written
twice; there is one list, rendered once, measured rather than hand-padded.
The generated CLI reference and the faucet manifest compose through the
same function.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-30 15:05:42 -07:00

669 lines
22 KiB
Go

// Copyright (C) 2022-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package chaincmd
import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"net"
"net/http"
"os"
"path/filepath"
"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/keychain"
"github.com/luxfi/cli/pkg/localnetworkinterface"
"github.com/luxfi/cli/pkg/route"
"github.com/luxfi/cli/pkg/utils"
"github.com/luxfi/cli/pkg/ux"
"github.com/luxfi/evm/core"
"github.com/luxfi/sdk/models"
"github.com/spf13/cobra"
)
// Default timeouts for chain deployment
const (
// DefaultDeployTimeout is the maximum time to wait for chain deployment to complete.
// For local networks, this should be fast (<30s). Longer means something is wrong.
DefaultDeployTimeout = 30 * time.Second
// MaxConsecutiveHealthFailures is the number of consecutive health check failures before failing fast
MaxConsecutiveHealthFailures = 10
// LuxEVMName is the canonical name for the Lux EVM
LuxEVMName = "Lux EVM"
// RemoteProbeTimeout is the timeout for probing a remote network endpoint
RemoteProbeTimeout = 30 * time.Second
)
var (
deployLocal bool
deployTestnet bool
deployMainnet bool
deployDevnet bool
nodeVersion string
deployTimeout time.Duration
deployKeyName string
)
func newDeployCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "deploy [chainName]",
Short: "Deploy a blockchain to local network, testnet, or mainnet",
Long: `Deploy a configured blockchain to the network.
OVERVIEW:
Deploys a blockchain configuration to a running network. The blockchain
must be created first with 'lux chain create'. The target network must
be running before deployment.
NETWORK FLAGS (choose one):
--mainnet, -m Deploy to mainnet (port 9630, Network ID 1)
--testnet, -t Deploy to testnet (port 9640, Network ID 2)
--devnet, -d Deploy to devnet (port 9650, Network ID 3)
--local, -l Deploy to local/custom network
Default: --local (deploys to custom/local network)
PREREQUISITES:
1. Chain must be created:
lux chain create mychain
2. For local networks, network must be running:
lux network start --devnet
3. For remote networks (devnet, testnet, mainnet), a funded key is needed:
Set MNEMONIC or PRIVATE_KEY env var, or use --key flag
4. VM must be installed (for custom VMs):
lux vm link "Lux EVM" --path ~/work/lux/evm/build/evm
OPTIONS:
--node-version Specific luxd version to use (default: latest)
--key Key name for remote network deployment (from ~/.lux/keys/)
EXAMPLES:
# Deploy to remote devnet (auto-detects remote endpoint)
lux chain deploy mychain --devnet
# Deploy to remote devnet with specific key
lux chain deploy mychain --devnet --key mykey
# Deploy to local devnet (if local network is running)
lux chain deploy mychain --devnet
# Deploy to testnet
lux chain deploy mychain --testnet
lux chain deploy mychain -t
# Deploy to mainnet
lux chain deploy mychain --mainnet
lux chain deploy mychain -m
# Deploy with specific node version
lux chain deploy mychain --devnet --node-version v1.11.0
DEPLOYMENT PROCESS:
Local network:
1. Validates chain configuration exists
2. Verifies local gRPC network is running
3. Checks VM plugin is installed
4. Creates blockchain via netrunner gRPC
5. Updates sidecar with deployment info
Remote network:
1. Validates chain configuration exists
2. Probes remote endpoint (e.g., https://api.lux-dev.network)
3. Creates chain on P-chain via wallet transaction
4. Creates blockchain on P-chain via wallet transaction
5. Updates sidecar with deployment info
OUTPUT:
On success, displays:
- Blockchain ID
- Chain ID
- RPC endpoints
TROUBLESHOOTING:
"Network not running" → Start network first:
lux network start --devnet
"Chain mychain not found" → Create chain first:
lux chain create mychain
"VM not installed" → Link VM binary:
lux vm link "Lux EVM" --path ~/path/to/evm
"RPC version mismatch" → Chain VM version incompatible with running node
NOTES:
- Deployment info is saved to the chain's sidecar.json
- Same chain can be deployed to multiple networks
- Each deployment gets unique blockchain ID
- Use 'lux network status' to see deployed chain endpoints`,
SilenceUsage: true,
Args: cobra.ExactArgs(1),
RunE: deployChain,
}
cmd.Flags().BoolVarP(&deployLocal, "local", "l", false, "Deploy to local/custom network")
cmd.Flags().BoolVarP(&deployTestnet, "testnet", "t", false, "Deploy to testnet")
cmd.Flags().BoolVarP(&deployMainnet, "mainnet", "m", false, "Deploy to mainnet")
cmd.Flags().BoolVarP(&deployDevnet, "devnet", "d", false, "Deploy to devnet")
cmd.Flags().StringVar(&nodeVersion, "node-version", "latest", "Node version to use")
cmd.Flags().DurationVar(&deployTimeout, "timeout", DefaultDeployTimeout, "Maximum time to wait for chain deployment (e.g., 60s, 2m)")
cmd.Flags().StringVar(&deployKeyName, "key", "", "Key name for remote network deployment (from ~/.lux/keys/)")
return cmd
}
func deployChain(cmd *cobra.Command, args []string) error {
chainName := args[0]
// Load sidecar
sc, err := app.LoadSidecar(chainName)
if err != nil {
err = fmt.Errorf("chain %s not found. Create it first with: lux chain create %s", chainName, chainName)
ux.Logger.PrintError("%s", err)
return err
}
// Load genesis
chainGenesis, err := app.LoadRawGenesis(chainName)
if err != nil {
err = fmt.Errorf("failed to load genesis: %w", err)
ux.Logger.PrintError("%s", err)
return err
}
// Validate genesis
if sc.VM == models.EVM {
var genesis core.Genesis
if err := json.Unmarshal(chainGenesis, &genesis); err != nil {
err = fmt.Errorf("invalid genesis format: %w", err)
ux.Logger.PrintError("%s", err)
return err
}
}
// Determine network
var network models.Network
switch {
case deployMainnet:
network = models.Mainnet
case deployTestnet:
network = models.Testnet
case deployDevnet:
network = models.Devnet
case deployLocal:
network = models.Local
default:
network = models.Local // Default to local
}
ux.Logger.PrintToUser("Deploying %s to %s", chainName, network.String())
// All deployments use the same flow - deploy to locally running network
if err := deployToNetwork(chainName, chainGenesis, &sc, network); err != nil {
ux.Logger.PrintError("%s", err)
return err
}
return nil
}
// verifyVMInstalled checks that the VM plugin is installed before deployment.
// Returns nil if VM is ready, otherwise returns an actionable error.
func verifyVMInstalled(chainName string, sc *models.Sidecar) error {
// Get the actual VM name based on VM type
// The VMID is computed from the VM name, not the chain name
vmName := LuxEVMName // Default for EVM chains
if sc.VM == models.CustomVM {
vmName = chainName // For custom VMs, use chain name
}
// Compute VMID from VM name
vmID, err := utils.VMID(vmName)
if err != nil {
return fmt.Errorf("failed to compute VMID for VM %s: %w", vmName, err)
}
vmIDStr := vmID.String()
// Get plugins directory path - plugins/current is the active plugins directory
pluginPath := filepath.Join(app.GetCurrentPluginsDir(), vmIDStr)
app.Log.Debug("Checking plugin path", "path", pluginPath, "vmid", vmIDStr, "pluginsDir", app.GetCurrentPluginsDir())
// Check if plugin exists
info, err := os.Lstat(pluginPath)
if os.IsNotExist(err) {
// Plugin does not exist - provide actionable error
displayName := getVMDisplayName(sc.VM)
return fmt.Errorf(`VM '%s' not installed (VMID: %s)
To fix, run:
lux vm link "%s" --path ~/work/lux/evm/build/evm`,
displayName, vmIDStr, vmName)
}
if err != nil {
return fmt.Errorf("failed to check VM plugin at %s: %w", pluginPath, err)
}
// Check if it's a symlink with a missing target
if info.Mode()&os.ModeSymlink != 0 {
target, err := os.Readlink(pluginPath)
if err != nil {
return fmt.Errorf("failed to read symlink %s: %w", pluginPath, err)
}
// Resolve target path (handle relative symlinks)
if !filepath.IsAbs(target) {
target = filepath.Join(app.GetCurrentPluginsDir(), target)
}
if _, err := os.Stat(target); os.IsNotExist(err) {
vmName := getVMDisplayName(sc.VM)
return fmt.Errorf(`VM '%s' symlink exists but target is missing (VMID: %s)
Plugin symlink: %s
Target (missing): %s
To fix, update the symlink:
rm %s
lux vm link %s --path <path-to-vm-binary>`,
vmName, vmIDStr,
pluginPath, target,
pluginPath, chainName)
}
}
// Verify it's executable
if info.Mode()&0o111 == 0 {
return fmt.Errorf("VM plugin at %s is not executable", pluginPath)
}
app.Log.Debug("VM plugin verified", "vmid", vmIDStr, "path", pluginPath)
return nil
}
// getVMDisplayName returns a human-readable name for the VM type
func getVMDisplayName(vm models.VMType) string {
switch vm {
case models.EVM:
return LuxEVMName
case models.CustomVM:
return "Custom VM"
default:
return string(vm)
}
}
// getRemoteEndpoint returns the well-known remote API endpoint for a network type.
// Returns empty string for local/custom networks that have no remote endpoint.
func getRemoteEndpoint(network models.Network) string {
if ovr := os.Getenv("NODE_ENDPOINT"); ovr != "" {
return ovr
}
return network.Endpoint()
}
// probeRemoteEndpoint checks if a remote network endpoint is alive by hitting /v1/info.
// Returns true if the endpoint responds to a JSON-RPC request.
func probeRemoteEndpoint(endpoint string) bool {
ctx, cancel := context.WithTimeout(context.Background(), RemoteProbeTimeout)
defer cancel()
url := endpoint + "/v1/info"
body := []byte(`{"jsonrpc":"2.0","method":"info.getNodeVersion","params":{},"id":1}`)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
if err != nil {
return false
}
req.Header.Set("Content-Type", "application/json")
// Use a client with short per-IP dial timeout and skip TLS verify.
// DNS may return multiple IPs where some are unreachable.
dialer := &net.Dialer{Timeout: 5 * time.Second}
client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
DialContext: dialer.DialContext,
ForceAttemptHTTP2: true,
},
}
resp, err := client.Do(req)
if err != nil {
ux.Logger.PrintToUser(" probe error: %v", err)
return false
}
defer resp.Body.Close()
// Any response (even 4xx for missing method) means the node is alive
return resp.StatusCode < 500
}
// isRemoteCapableNetwork returns true if the network can be deployed to via remote P-chain API
func isRemoteCapableNetwork(network models.Network) bool {
return network == models.Devnet || network == models.Testnet || network == models.Mainnet
}
func deployToNetwork(chainName string, chainGenesis []byte, sc *models.Sidecar, network models.Network) error {
app.Log.Debug("Deploy to network", "network", network.String())
// Map deploy target to network type
// Default is "custom" (not "local" which is ambiguous - any network can run locally)
targetType := "custom"
switch network {
case models.Testnet:
targetType = "testnet"
case models.Mainnet:
targetType = "mainnet"
case models.Devnet:
targetType = "devnet"
case models.Local:
targetType = "custom"
}
// Load network state for the specific target network type
// Each network type (custom, testnet, mainnet) has its own state file
networkState, stateErr := app.LoadNetworkStateForType(targetType)
if stateErr != nil {
// State file read error (not just missing) - only fail if no remote fallback
if !isRemoteCapableNetwork(network) {
return fmt.Errorf("failed to load network state: %w\nIs the network running? Start with: lux network start", stateErr)
}
app.Log.Debug("Failed to load network state, will try remote endpoint", "error", stateErr)
networkState = nil
}
// "custom" (= models.Local, network-id 1337) and "dev" (`lux network start
// --dev`, also network-id 1337) are the same concept: a local-only network
// keyed by LIGHT_MNEMONIC. If the operator started a --dev network, accept
// it for `lux chain deploy --local`.
if targetType == "custom" && (networkState == nil || !networkState.Running) {
if devState, _ := app.LoadNetworkStateForType("dev"); devState != nil && devState.Running {
networkState = devState
}
}
// If NODE_ENDPOINT is set for --local, bypass gRPC and deploy directly via P-chain API.
if !isRemoteCapableNetwork(network) {
if ovr := os.Getenv("NODE_ENDPOINT"); ovr != "" {
ux.Logger.PrintToUser("NODE_ENDPOINT override: deploying directly to %s", ovr)
return deployToRemoteNetwork(chainName, chainGenesis, sc, network, ovr)
}
}
// For remote-capable networks, try the remote endpoint if:
// 1. No local state exists, OR
// 2. Local state exists but has a remote API endpoint (e.g., https://...), OR
// 3. Local state claims running but the state file is stale (gRPC server dead)
if isRemoteCapableNetwork(network) {
// For devnet/testnet/mainnet, ALWAYS try the remote endpoint first.
// These are real networks at api.lux-dev.network, api.lux-test.network,
// api.lux.network — not local. Local state is irrelevant.
remoteEndpoint := getRemoteEndpoint(network)
if remoteEndpoint != "" {
ux.Logger.PrintToUser("Probing remote %s endpoint: %s", targetType, remoteEndpoint)
if probeRemoteEndpoint(remoteEndpoint) {
ux.Logger.PrintToUser("Remote %s is alive at %s", targetType, remoteEndpoint)
return deployToRemoteNetwork(chainName, chainGenesis, sc, network, remoteEndpoint)
}
ux.Logger.PrintToUser("Remote endpoint %s is not reachable, falling back to local network", remoteEndpoint)
}
}
// Local network path - requires running gRPC netrunner
if networkState == nil || !networkState.Running {
startHint := "lux network start"
switch targetType {
case "testnet":
startHint = "lux network start --testnet"
case "mainnet":
startHint = "lux network start --mainnet"
case "devnet":
startHint = "lux network start --devnet"
}
return fmt.Errorf("no %s network running. Start the network first with: %s", targetType, startHint)
}
return deployToLocalNetwork(chainName, chainGenesis, sc, network, networkState)
}
// deployToLocalNetwork deploys a chain to a locally-running network managed by the CLI's gRPC netrunner.
func deployToLocalNetwork(chainName string, chainGenesis []byte, sc *models.Sidecar, network models.Network, networkState *application.NetworkState) error {
// Log gRPC port being used
app.Log.Debug("Using gRPC port from network state", "port", networkState.GRPCPort, "network", networkState.NetworkType)
// Preflight check: verify VM is installed before any network operations
if err := verifyVMInstalled(chainName, sc); err != nil {
return err
}
// Get VM binary - prefer linked plugin over downloaded
var vmBin string
var err error
// Compute VMID for plugin lookup
vmName := LuxEVMName
if sc.VM == models.CustomVM {
vmName = chainName
}
vmID, _ := utils.VMID(vmName)
vmIDStr := vmID.String()
switch sc.VM {
case models.EVM:
// First check if EVM plugin already exists (linked or copied)
pluginPath := filepath.Join(app.GetCurrentPluginsDir(), vmIDStr)
if info, pluginErr := os.Stat(pluginPath); pluginErr == nil && info.Mode().IsRegular() && info.Mode()&0o111 != 0 {
// Plugin exists and is executable, use it directly
vmBin = pluginPath
app.Log.Debug("Using existing EVM plugin", "path", vmBin)
} else {
// Fall back to downloading
vmBin, err = binutils.SetupEVM(app, sc.VMVersion)
if err != nil {
return fmt.Errorf("failed to setup EVM: %w", err)
}
}
case models.CustomVM:
vmBin = binutils.SetupCustomBin(app, chainName)
default:
return fmt.Errorf("unknown VM type: %s", sc.VM)
}
// Check RPC version compatibility
if sc.VM != models.CustomVM {
// Use app-aware status checker to detect the correct running network endpoint
nc := localnetworkinterface.NewStatusCheckerWithApp(app)
nodeVersion, err = checkDeployCompatibility(nc, sc.RPCVersion)
if err != nil {
return fmt.Errorf("RPC version check failed: %w", err)
}
}
// Create deployer with network-aware gRPC client
// This ensures we connect to the correct gRPC server for the running network
deployer := chain.NewLocalDeployerForNetwork(app, nodeVersion, vmBin, networkState.NetworkType)
// Get genesis path
genesisPath := app.GetGenesisPath(chainName)
// Deploy to locally-running network (works for local, testnet, mainnet started via CLI)
chainID, blockchainID, err := deployer.DeployToLocalNetwork(chainName, chainGenesis, genesisPath)
if err != nil {
// Check if this is a DeploymentError (chain-specific failure)
var deployErr *chain.DeploymentError
if errors.As(err, &deployErr) {
// Deployment failed but we can provide useful feedback
ux.Logger.PrintError("\nChain deployment failed: %s", deployErr.Cause)
if deployErr.NetworkHealthy {
ux.Logger.PrintToUser("\nThe primary network is still running. You can:")
ux.Logger.PrintToUser(" 1. Fix the issue and retry: lux chain deploy %s", chainName)
ux.Logger.PrintToUser(" 2. Check logs: lux network status")
ux.Logger.PrintToUser(" 3. Stop the network: lux network stop")
} else {
ux.Logger.PrintError("\nThe network may have crashed. Check logs and restart:")
ux.Logger.PrintToUser(" 1. lux network stop")
ux.Logger.PrintToUser(" 2. lux network start --%s", network.String())
}
return err
}
// Non-deployment error (gRPC connection issue, etc)
if deployer.BackendStartedHere() {
if innerErr := binutils.KillgRPCServerProcessForNetwork(app, networkState.NetworkType); innerErr != nil {
app.Log.Warn("failed to kill gRPC server", "error", innerErr)
}
}
return fmt.Errorf("deployment failed: %w", err)
}
// Update sidecar with deployment info (using the target network)
if err := app.UpdateSidecarNetworks(sc, network, chainID, blockchainID); err != nil {
return fmt.Errorf("failed to update sidecar: %w", err)
}
return nil
}
// deployToRemoteNetwork deploys a chain to a remote network via P-chain API transactions.
// This is used when no local gRPC netrunner is running but the remote network is reachable.
func deployToRemoteNetwork(chainName string, chainGenesis []byte, sc *models.Sidecar, network models.Network, endpoint string) error {
ux.Logger.PrintToUser("Deploying to remote %s via P-chain API at %s", network.String(), endpoint)
// Get keychain for signing P-chain transactions
networkID := network.ID()
kc, err := getDeployKeychain(network, networkID)
if err != nil {
return fmt.Errorf("failed to get keychain for deployment: %w\n\nTo fix, set MNEMONIC or PRIVATE_KEY env var, or use --key flag", err)
}
// Show the deployer address
addrs := kc.Keychain.Addresses().List()
if len(addrs) == 0 {
return fmt.Errorf("keychain has no addresses")
}
// Create the public deployer
deployer := chain.NewPublicDeployer(app, kc.UsesLedger, kc.Keychain, network)
// Step 1: Create chain (P-chain transaction)
ux.Logger.PrintToUser("Creating chain on P-chain...")
controlKeys, err := kc.PChainFormattedStrAddresses()
if err != nil {
return fmt.Errorf("failed to get P-chain addresses: %w", err)
}
ux.Logger.PrintToUser("Control keys: %v", controlKeys)
chainID, err := deployer.DeployChain(controlKeys, uint32(len(controlKeys)))
if err != nil {
return fmt.Errorf("failed to create chain: %w", err)
}
ux.Logger.PrintToUser("Chain created: %s", chainID.String())
// Resolve the VM name for VMID computation. The VMID is derived from the
// VM name, not the blockchain name. For EVM chains the canonical VM name
// is "Lux EVM"; for custom VMs it is the chain name itself.
vmName := chainName
if sc.VM == models.EVM {
vmName = LuxEVMName
}
// Step 2: Create blockchain (P-chain transaction)
// Pass chainName as the display name and vmName as the VMID source.
ux.Logger.PrintToUser("Creating blockchain on chain %s...", chainID.String())
isFullySigned, blockchainID, _, _, err := deployer.DeployBlockchain(
controlKeys,
controlKeys,
chainID,
chainName,
chainGenesis,
vmName,
)
if err != nil {
return fmt.Errorf("failed to create blockchain: %w", err)
}
if !isFullySigned {
return fmt.Errorf("blockchain transaction requires additional signatures (multisig not yet supported for remote deploy)")
}
ux.Logger.PrintToUser("")
ux.Logger.PrintToUser("Blockchain deployed successfully!")
ux.Logger.PrintToUser(" Chain ID: %s", chainID.String())
ux.Logger.PrintToUser(" Blockchain ID: %s", blockchainID.String())
ux.Logger.PrintToUser(" RPC Endpoint: %s", route.Chain(endpoint, blockchainID.String())+"/rpc")
ux.Logger.PrintToUser("")
// Update sidecar with deployment info
if err := app.UpdateSidecarNetworks(sc, network, chainID, blockchainID); err != nil {
return fmt.Errorf("failed to update sidecar: %w", err)
}
return nil
}
// getDeployKeychain obtains a keychain for remote network deployment.
// Priority:
// 1. --key flag (explicit key name)
// 2. PRIVATE_KEY env var
// 3. MNEMONIC env var
// 4. Interactive prompt (if terminal available)
func getDeployKeychain(network models.Network, networkID uint32) (*keychain.Keychain, error) {
// If --key flag specified, use that key
if deployKeyName != "" {
return keychain.GetKeychain(app, false, false, nil, deployKeyName, network, 0)
}
// Try environment variables (PRIVATE_KEY, MNEMONIC)
sf, err := key.GetOrCreateLocalKey(networkID)
if err == nil && sf != nil {
kc := sf.KeyChain()
wrappedKc := keychain.WrapSecp256k1fxKeychain(kc)
pAddrs := sf.P()
if len(pAddrs) > 0 {
ux.Logger.PrintToUser("Using key with P-Chain address: %s", pAddrs[0])
}
return keychain.NewKeychain(network, wrappedKc, nil, nil), nil
}
// Fall back to interactive prompt via GetKeychainFromCmdLineFlags
return keychain.GetKeychainFromCmdLineFlags(app, "deploy chain to "+network.String(), network, "", false, false, nil, 0)
}
func checkDeployCompatibility(network localnetworkinterface.StatusChecker, configuredRPCVersion int) (string, error) {
runningVersion, runningRPCVersion, networkRunning, err := network.GetCurrentNetworkVersion()
if err != nil {
return "", err
}
if networkRunning {
if nodeVersion == "latest" {
if runningRPCVersion != configuredRPCVersion {
return "", fmt.Errorf(
"running node uses RPC version %d but chain requires %d",
runningRPCVersion,
configuredRPCVersion,
)
}
return runningVersion, nil
}
if runningVersion != nodeVersion {
return "", fmt.Errorf("incompatible node version: running %s, requested %s", runningVersion, nodeVersion)
}
}
return nodeVersion, nil
}