Files
zeekay 8f56009cc7 fix: deep audit - remove stubs, fix imports, clean dead code
Deep audit changes:
- Replace stub signature aggregator with real SDK warp implementation
- Remove dead pkg/lpm package (SDK lpmintegration used instead)
- Fix fs imports: use local utils.FileExists, DirExists, ExpandHome
- Fix secp256k1fx imports: use node/vms/secp256k1fx
- Fix platformvm imports: use node/vms/platformvm
- Fix e2e test TODOs: uncomment BootstrapValidators assertions
- Improve localnet stub implementations with real logic
- Fix node.go fs.FileExists reference

Note: Build requires GOWORK=off due to workspace dependency issues
2026-01-10 11:17:00 -08:00

432 lines
12 KiB
Go

// Copyright (C) 2022-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package upgradecmd
import (
"errors"
"fmt"
"github.com/luxfi/cli/pkg/binutils"
"github.com/luxfi/cli/pkg/chain"
"github.com/luxfi/cli/pkg/cobrautils"
"github.com/luxfi/cli/pkg/plugins"
"github.com/luxfi/cli/pkg/prompts"
"github.com/luxfi/cli/pkg/utils"
"github.com/luxfi/cli/pkg/ux"
"github.com/luxfi/cli/pkg/vm"
"github.com/luxfi/constants"
"github.com/luxfi/netrunner/server"
"github.com/luxfi/sdk/models"
"github.com/spf13/cobra"
)
const (
futureDeployment = "Update config for future deployments"
localDeployment = "Existing local deployment"
testnetDeployment = "Testnet"
mainnetDeployment = "Mainnet"
)
var (
pluginDir string
useTestnet bool
useMainnet bool
useLocal bool
useConfig bool
useManual bool
useLatest bool
targetVersion string
binaryPathArg string
)
// lux blockchain upgrade vm
func newUpgradeVMCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "vm [blockchainName]",
Short: "Upgrade a blockchain's binary",
Long: `The blockchain upgrade vm command enables the user to upgrade their Blockchain's VM binary. The command
can upgrade both local Blockchains and publicly deployed Blockchains on Testnet and Mainnet.
The command walks the user through an interactive wizard. The user can skip the wizard by providing
command line flags.`,
RunE: upgradeVM,
Args: cobrautils.ExactArgs(1),
}
cmd.Flags().BoolVar(&useConfig, "config", false, "upgrade config for future chain deployments")
cmd.Flags().BoolVar(&useLocal, "local", false, "upgrade existing `local` deployment")
cmd.Flags().BoolVar(&useTestnet, "testnet", false, "upgrade existing `testnet` deployment (alias for `testnet`)")
cmd.Flags().BoolVar(&useMainnet, "mainnet", false, "upgrade existing `mainnet` deployment")
cmd.Flags().BoolVar(&useManual, "print", false, "print instructions for upgrading")
cmd.Flags().StringVar(&pluginDir, "plugin-dir", "", "plugin directory to automatically upgrade VM")
cmd.Flags().BoolVar(&useLatest, "latest", false, "upgrade to latest version")
cmd.Flags().StringVar(&targetVersion, "version", "", "Upgrade to custom version")
cmd.Flags().StringVar(&binaryPathArg, "binary", "", "Upgrade to custom binary")
return cmd
}
func atMostOneNetworkSelected() bool {
count := 0
for _, selected := range []bool{useConfig, useLocal, useTestnet, useMainnet} {
if selected {
count++
}
}
return count <= 1
}
func atMostOneVersionSelected() bool {
count := 0
for _, selected := range []bool{useLatest, targetVersion != "", binaryPathArg != ""} {
if selected {
count++
}
}
return count <= 1
}
func atMostOneAutomationSelected() bool {
return !useManual || pluginDir == ""
}
func upgradeVM(_ *cobra.Command, args []string) error {
// Check flag preconditions
if !atMostOneNetworkSelected() {
return errors.New("too many networks selected")
}
if !atMostOneVersionSelected() {
return errors.New("too many versions selected")
}
if !atMostOneAutomationSelected() {
return errors.New("--print and --plugin-dir are mutually exclusive")
}
blockchainName := args[0]
if !app.BlockchainConfigExists(blockchainName) {
return errors.New("blockchain does not exist")
}
sc, err := app.LoadSidecar(blockchainName)
if err != nil {
return fmt.Errorf("unable to load sidecar: %w", err)
}
upgradeOptions := []string{futureDeployment}
networkToUpgrade, err := selectNetworkToUpgrade(sc, upgradeOptions)
if err != nil {
return err
}
// if upgrading local, check that the network is off otherwise fail here
serverRunning, err := isServerRunning()
if err != nil {
return err
}
if serverRunning {
ux.Logger.PrintToUser("Please stop network before upgrading local VMs")
return errors.New("network is still running")
}
vmType := sc.VM
if vmType == models.EVM {
return selectUpdateOption(vmType, sc, networkToUpgrade)
}
// Must be a custom update
return updateToCustomBin(sc, networkToUpgrade, binaryPathArg, true)
}
// select which network to upgrade
// optionally provide a list of options to preload
func selectNetworkToUpgrade(sc models.Sidecar, upgradeOptions []string) (string, error) {
switch {
case useConfig:
return futureDeployment, nil
case useLocal:
return localDeployment, nil
case useTestnet:
return testnetDeployment, nil
case useMainnet:
return mainnetDeployment, nil
}
updatePrompt := "What deployment would you like to upgrade"
if upgradeOptions == nil {
upgradeOptions = []string{}
}
// get locally deployed chains from file since network is shut down
locallyDeployedChains, err := chain.GetLocallyDeployedChainsFromFile(app)
if err != nil {
return "", fmt.Errorf("unable to read deployed chains: %w", err)
}
for _, chain := range locallyDeployedChains {
if chain == sc.Name {
upgradeOptions = append(upgradeOptions, localDeployment)
}
}
// check if chain deployed on testnet
if _, ok := sc.Networks[models.Testnet.String()]; ok {
upgradeOptions = append(upgradeOptions, testnetDeployment)
}
// check if chain deployed on mainnet
if _, ok := sc.Networks[models.Mainnet.String()]; ok {
upgradeOptions = append(upgradeOptions, mainnetDeployment)
}
if len(upgradeOptions) == 0 {
return "", errors.New("no deployment target available")
}
if !prompts.IsInteractive() {
// In non-interactive mode, use the first available option or require explicit flag
return "", fmt.Errorf("network selection required: use --config, --local, --testnet, or --mainnet")
}
selectedDeployment, err := app.Prompt.CaptureList(updatePrompt, upgradeOptions)
if err != nil {
return "", err
}
return selectedDeployment, nil
}
func selectUpdateOption(vmType models.VMType, sc models.Sidecar, networkToUpgrade string) error {
switch {
case useLatest:
return updateToLatestVersion(vmType, sc, networkToUpgrade)
case targetVersion != "":
return updateToSpecificVersion(sc, networkToUpgrade)
case binaryPathArg != "":
return updateToCustomBin(sc, networkToUpgrade, binaryPathArg, true)
}
// In non-interactive mode, require explicit version flag
if !prompts.IsInteractive() {
return fmt.Errorf("version selection required: use --latest, --version, or --binary")
}
latestVersionUpdate := "Update to latest version"
specificVersionUpdate := "Update to a specific version"
customBinaryUpdate := "Update to a custom binary"
updateOptions := []string{latestVersionUpdate, specificVersionUpdate, customBinaryUpdate}
updatePrompt := "How would you like to update your chain's virtual machine"
updateDecision, err := app.Prompt.CaptureList(updatePrompt, updateOptions)
if err != nil {
return err
}
switch updateDecision {
case latestVersionUpdate:
return updateToLatestVersion(vmType, sc, networkToUpgrade)
case specificVersionUpdate:
return updateToSpecificVersion(sc, networkToUpgrade)
case customBinaryUpdate:
return updateToCustomBin(sc, networkToUpgrade, binaryPathArg, true)
default:
return errors.New("invalid option")
}
}
func updateToLatestVersion(vmType models.VMType, sc models.Sidecar, networkToUpgrade string) error {
// pull in current version
currentVersion := sc.VMVersion
// check latest version
releaseURL := fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/latest", constants.LuxOrg, vmType.RepoName())
latestVersion, err := app.Downloader.GetLatestReleaseVersion(releaseURL)
if err != nil {
return err
}
// check if current version equals latest
if currentVersion == "latest" || currentVersion == latestVersion {
ux.Logger.PrintToUser("VM already up-to-date")
return nil
}
return updateVMByNetwork(sc, latestVersion, networkToUpgrade)
}
func updateToSpecificVersion(sc models.Sidecar, networkToUpgrade string) error {
// pull in current version
currentVersion := sc.VMVersion
// Get version to update to
var err error
if targetVersion == "" {
if !prompts.IsInteractive() {
return fmt.Errorf("--version is required in non-interactive mode")
}
targetVersion, err = app.Prompt.CaptureVersion("Enter version")
if err != nil {
return err
}
}
// check if current version equals chosen version
if currentVersion == targetVersion {
ux.Logger.PrintToUser("VM already up-to-date")
return nil
}
return updateVMByNetwork(sc, targetVersion, networkToUpgrade)
}
func updateVMByNetwork(sc models.Sidecar, targetVersion string, networkToUpgrade string) error {
switch networkToUpgrade {
case futureDeployment:
return updateFutureVM(sc, targetVersion)
case localDeployment:
return updateExistingLocalVM(sc, targetVersion)
case testnetDeployment:
return chooseManualOrAutomatic(sc, targetVersion)
case mainnetDeployment:
return chooseManualOrAutomatic(sc, targetVersion)
default:
return errors.New("unknown deployment")
}
}
func updateToCustomBin(sc models.Sidecar, networkToUpgrade, binaryPath string, updateVMBinaryProtocolVersion bool) error {
var err error
if binaryPath == "" {
if !prompts.IsInteractive() {
return fmt.Errorf("--binary is required in non-interactive mode")
}
binaryPath, err = app.Prompt.CaptureExistingFilepath("Enter path to custom binary")
if err != nil {
return err
}
}
if err := app.CopyVMBinary(binaryPath, sc.Name); err != nil {
return err
}
sc.VM = models.CustomVM
if updateVMBinaryProtocolVersion {
sc.RPCVersion, err = vm.GetVMBinaryProtocolVersion(binaryPath)
if err != nil {
return fmt.Errorf("unable to get RPC version: %w", err)
}
}
targetVersion := ""
return updateVMByNetwork(sc, targetVersion, networkToUpgrade)
}
func updateFutureVM(sc models.Sidecar, targetVersion string) error {
// to switch to new version, just need to update sidecar
sc.VMVersion = targetVersion
if err := app.UpdateSidecar(&sc); err != nil {
return err
}
ux.Logger.PrintToUser("VM updated for future deployments. Update will apply next time chain is deployed.")
return nil
}
func updateExistingLocalVM(sc models.Sidecar, targetVersion string) error {
vmid, err := utils.VMID(sc.Name)
if err != nil {
return err
}
var vmBin string
switch sc.VM {
// download the binary and prepare to copy it
case models.EVM:
vmBin, err = binutils.SetupEVM(app, targetVersion)
if err != nil {
return fmt.Errorf("failed to install evm: %w", err)
}
case models.CustomVM:
// get the path to the already copied binary
vmBin = binutils.SetupCustomBin(app, sc.Name)
default:
return errors.New("unknown VM type " + string(sc.VM))
}
rpcVersion, err := vm.GetVMBinaryProtocolVersion(vmBin)
if err != nil {
return fmt.Errorf("unable to get RPC version: %w", err)
}
// Update the binary in the plugin directory
if err := binutils.UpgradeVM(app, vmid.String(), vmBin); err != nil {
return err
}
// Update the sidecar with new RPC version
if err = binutils.UpdateLocalSidecarRPC(app, sc, rpcVersion); err != nil {
return fmt.Errorf("unable to set RPC version: %w", err)
}
ux.Logger.PrintToUser("Upgrade complete. Ready to restart the network.")
return nil
}
func chooseManualOrAutomatic(sc models.Sidecar, targetVersion string) error {
switch {
case useManual:
return plugins.ManualUpgrade(app, sc, targetVersion)
case pluginDir != "":
return plugins.AutomatedUpgrade(app, sc, targetVersion, pluginDir)
}
// In non-interactive mode, require explicit choice
if !prompts.IsInteractive() {
return fmt.Errorf("upgrade method required: use --print for manual or --plugin-dir for automatic")
}
const (
choiceManual = "Manual"
choiceAutomatic = "Automatic (Make sure your node isn't running)"
)
choice, err := app.Prompt.CaptureList(
"How would you like to update the luxd config?",
[]string{choiceAutomatic, choiceManual},
)
if err != nil {
return err
}
if choice == choiceManual {
return plugins.ManualUpgrade(app, sc, targetVersion)
}
return plugins.AutomatedUpgrade(app, sc, targetVersion, pluginDir)
}
func isServerRunning() (bool, error) {
cli, err := binutils.NewGRPCClient()
if errors.Is(err, binutils.ErrGRPCTimeout) {
return false, nil
} else if err != nil {
return false, err
}
ctx, cancel := utils.GetAPIContext()
defer cancel()
_, err = cli.Status(ctx)
if err == nil || !server.IsServerError(err, server.ErrNotBootstrapped) {
return true, nil
}
return false, nil
}