Files

324 lines
11 KiB
Go

// Copyright (C) 2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package localnet
import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/luxfi/cli/v2/pkg/application"
"github.com/luxfi/cli/v2/pkg/binutils"
"github.com/luxfi/cli/v2/pkg/constants"
"github.com/luxfi/cli/v2/pkg/interchain/relayer"
"github.com/luxfi/cli/v2/pkg/models"
"github.com/luxfi/cli/v2/pkg/utils"
sdkutils "github.com/luxfi/cli/v2/sdk/utils"
"github.com/luxfi/netrunner/network"
luxdconfig "github.com/luxfi/node/v2/v2/config"
"github.com/luxfi/ids"
dircopy "github.com/otiai10/copy"
)
const migratedSuffix = "-migrated"
// Called from 'internal/migrations' previously to any command execution
// Iterates over all local clusters, finding if there is a legacy network runner one
// If that is the case, renames it with migratedSuffix, creates a new tmpnet cluster
// based on all network runner cluster info, and then remove the legacy one on success
// If the is a cluster running, first stops it, and any relayer associated with it,
// then migrate the cluster, then run thew new one again, together with the relayer
// Relayer stop/start is needed because a connected relayer makes bootstrapping to
// fail upon cluster restart
func MigrateANRToTmpNet(
app *application.Lux,
printFunc func(msg string, args ...interface{}),
) error {
ctx, cancel := utils.GetANRContext()
defer cancel()
var (
clusterToReload string
clusterToReloadNetwork models.Network
clusterToReloadHasRelayer bool
)
cli, _ := binutils.NewGRPCClientWithEndpoint(
binutils.LocalClusterGRPCServerEndpoint,
binutils.WithAvoidRPCVersionCheck(true),
binutils.WithDialTimeout(constants.FastGRPCDialTimeout),
)
if cli != nil {
// ANR is running
status, _ := cli.Status(ctx)
if status != nil && status.ClusterInfo != nil {
// there is a local cluster up
var err error
clusterToReload = filepath.Base(status.ClusterInfo.RootDataDir)
clusterToReloadNetwork = models.NetworkFromNetworkID(status.ClusterInfo.NetworkId)
clusterToReloadHasRelayer, _, _, err = relayer.RelayerIsUp(app.GetLocalRelayerRunPath(clusterToReloadNetwork.Kind))
if err != nil {
return nil
}
if clusterToReloadHasRelayer {
printFunc("Stopping relayer")
if err := relayer.RelayerCleanup(
app.GetLocalRelayerRunPath(clusterToReloadNetwork.Kind),
app.GetLocalRelayerLogPath(clusterToReloadNetwork.Kind),
app.GetLocalRelayerStorageDir(clusterToReloadNetwork.Kind),
); err != nil {
return err
}
}
printFunc("Found running cluster %s. Will restart after migration.", clusterToReload)
if _, err := cli.Stop(ctx); err != nil {
return fmt.Errorf("failed to stop luxd: %w", err)
}
}
if err := binutils.KillgRPCServerProcess(
app,
binutils.LocalClusterGRPCServerEndpoint,
constants.ServerRunFileLocalClusterPrefix,
); err != nil {
return err
}
}
toMigrate := []string{}
clustersDir := app.GetLocalClustersDir()
entries, err := os.ReadDir(clustersDir)
if err != nil {
return fmt.Errorf("failed to read local clusters dir %s: %w", clustersDir, err)
}
for _, entry := range entries {
if !entry.IsDir() {
continue
}
clusterName := entry.Name()
if _, err := GetLocalCluster(app, clusterName); err != nil {
// not tmpnet, or dir with failures
networkDir := filepath.Join(clustersDir, clusterName)
if strings.HasSuffix(clusterName, migratedSuffix) {
printFunc("%s was partially migrated with failure. Please manually recover", networkDir)
continue
}
jsonPath := filepath.Join(networkDir, "network.json")
if utils.FileExists(jsonPath) {
bs, err := os.ReadFile(jsonPath)
if err != nil {
printFunc("Failure loading JSON on cluster at %s: %s. Please manually recover", networkDir, err)
continue
}
var config network.Config
if err := json.Unmarshal(bs, &config); err != nil {
printFunc("Unexpected JSON format on cluster at %s: %s. Please manually recover", networkDir, err)
continue
}
toMigrate = append(toMigrate, clusterName)
} else {
printFunc("Unexpected format on cluster at %s. Please manually recover", networkDir)
}
}
}
for _, clusterName := range toMigrate {
printFunc("Migrating %s", clusterName)
if err := migrateCluster(app, printFunc, clusterName); err != nil {
printFunc("Failure migrating %s at %s: %s", clusterName, GetLocalClusterDir(app, clusterName), err)
}
}
if clusterToReload != "" {
printFunc("Restarting cluster %s.", clusterToReload)
if err := LoadLocalCluster(app, clusterToReload, ""); err != nil {
return err
}
if clusterToReloadHasRelayer {
localNetworkRootDir := ""
if clusterToReloadNetwork.Kind == models.Local {
localNetworkRootDir, err = GetLocalNetworkDir(app)
if err != nil {
return err
}
}
relayerConfigPath := app.GetLocalRelayerConfigPath(clusterToReloadNetwork.Kind, localNetworkRootDir)
relayerBinPath := ""
if clusterToReloadNetwork.Kind == models.Local {
if b, extraLocalNetworkData, err := GetExtraLocalNetworkData(app, ""); err != nil {
return err
} else if b {
relayerBinPath = extraLocalNetworkData.RelayerPath
}
}
if utils.FileExists(relayerConfigPath) {
printFunc("Restarting relayer")
if _, err := relayer.DeployRelayer(
constants.DefaultRelayerVersion,
relayerBinPath,
app.GetWarpRelayerBinDir(),
relayerConfigPath,
app.GetLocalRelayerLogPath(clusterToReloadNetwork.Kind),
app.GetLocalRelayerRunPath(clusterToReloadNetwork.Kind),
app.GetLocalRelayerStorageDir(clusterToReloadNetwork.Kind),
); err != nil {
return err
}
}
}
}
if len(toMigrate) > 0 {
printFunc("")
}
return nil
}
// Migrates a network runner cluster, by first renaming it, then creating
// a new tmpnet one based on it, then finally removing it on success
func migrateCluster(
app *application.Lux,
printFunc func(msg string, args ...interface{}),
clusterName string,
) error {
networkDir := GetLocalClusterDir(app, clusterName)
anrDir := GetLocalClusterDir(app, clusterName+migratedSuffix)
if err := os.Rename(networkDir, anrDir); err != nil {
return err
}
jsonPath := filepath.Join(anrDir, "network.json")
bs, err := os.ReadFile(jsonPath)
if err != nil {
return err
}
var config network.Config
if err := json.Unmarshal(bs, &config); err != nil {
return err
}
connectionSettings := ConnectionSettings{
NetworkID: config.NetworkID,
}
trackSubnetsStr := ""
nodeSettings := []NodeSetting{}
for _, nodeConfig := range config.NodeConfigs {
decodedStakingSigningKey, err := base64.StdEncoding.DecodeString(nodeConfig.StakingSigningKey)
if err != nil {
return err
}
httpPort, err := utils.GetJSONKey[float64](nodeConfig.Flags, luxdconfig.HTTPPortKey)
if err != nil {
return fmt.Errorf("failure reading legacy local network conf: %w", err)
}
stakingPort, err := utils.GetJSONKey[float64](nodeConfig.Flags, luxdconfig.StakingPortKey)
if err != nil {
return fmt.Errorf("failure reading legacy local network conf: %w", err)
}
trackSubnetsStr, err = utils.GetJSONKey[string](nodeConfig.Flags, luxdconfig.TrackSubnetsKey)
if err != nil && !errors.Is(err, constants.ErrKeyNotFoundOnMap) {
return fmt.Errorf("failure reading legacy local network conf: %w", err)
}
nodeSettings = append(nodeSettings, NodeSetting{
StakingTLSKey: []byte(nodeConfig.StakingKey),
StakingCertKey: []byte(nodeConfig.StakingCert),
StakingSignerKey: decodedStakingSigningKey,
HTTPPort: uint64(httpPort),
StakingPort: uint64(stakingPort),
})
}
var trackedSubnets []ids.ID
trackSubnetsStr = strings.TrimSpace(trackSubnetsStr)
if trackSubnetsStr != "" {
trackedSubnets, err = utils.MapWithError(strings.Split(trackSubnetsStr, ","), ids.FromString)
if err != nil {
return err
}
}
binPath := config.BinaryPath
// local connection info
networkModel := models.NetworkFromNetworkID(connectionSettings.NetworkID)
if networkModel.Kind == models.Local {
genesisPath := filepath.Join(anrDir, "node1", "configs", "genesis.json")
if !utils.FileExists(genesisPath) {
return fmt.Errorf("genesis path not found at %s for local network cluster", genesisPath)
}
connectionSettings.Genesis, err = os.ReadFile(genesisPath)
if err != nil {
return err
}
upgradePath := filepath.Join(anrDir, "node1", "configs", "upgrade.json")
if !utils.FileExists(upgradePath) {
return fmt.Errorf("upgrade path not found at %s for local network cluster", upgradePath)
}
connectionSettings.Upgrade, err = os.ReadFile(upgradePath)
if err != nil {
return err
}
for nodeID, nodeIP := range config.BeaconConfig {
connectionSettings.BootstrapIDs = append(connectionSettings.BootstrapIDs, nodeID.String())
connectionSettings.BootstrapIPs = append(connectionSettings.BootstrapIPs, nodeIP.String())
}
}
//
pluginDir := filepath.Join(networkDir, "plugins")
if err := os.MkdirAll(networkDir, constants.DefaultPerms755); err != nil {
return fmt.Errorf("could not create network directory %s: %w", networkDir, err)
}
if err := os.MkdirAll(pluginDir, constants.DefaultPerms755); err != nil {
return fmt.Errorf("could not create plugin directory %s: %w", pluginDir, err)
}
// defaultFlags
defaultFlags := map[string]interface{}{}
defaultFlags[luxdconfig.PartialSyncPrimaryNetworkKey] = true
defaultFlags[luxdconfig.NetworkAllowPrivateIPsKey] = true
defaultFlags[luxdconfig.IndexEnabledKey] = false
defaultFlags[luxdconfig.IndexAllowIncompleteKey] = true
network, err := CreateLocalCluster(
app,
printFunc,
clusterName,
binPath,
pluginDir,
defaultFlags,
connectionSettings,
uint32(len(nodeSettings)),
nodeSettings,
trackedSubnets,
networkModel,
false,
false,
)
if err != nil {
return err
}
for i, node := range network.Nodes {
sourceDir := filepath.Join(anrDir, config.NodeConfigs[i].Name, "db")
targetDir := filepath.Join(networkDir, node.NodeID.String(), "db")
if sdkutils.DirExists(sourceDir) {
if err := dircopy.Copy(sourceDir, targetDir); err != nil {
return fmt.Errorf("failure migrating data dir %s into %s: %w", sourceDir, targetDir, err)
}
}
sourceDir = filepath.Join(anrDir, config.NodeConfigs[i].Name, "chainData")
targetDir = filepath.Join(networkDir, node.NodeID.String(), "chainData")
if sdkutils.DirExists(sourceDir) {
if err := dircopy.Copy(sourceDir, targetDir); err != nil {
return fmt.Errorf("failure migrating data dir %s into %s: %w", sourceDir, targetDir, err)
}
}
sourceDir = filepath.Join(anrDir, config.NodeConfigs[i].Name, "plugins")
targetDir = filepath.Join(networkDir, "plugins")
if sdkutils.DirExists(sourceDir) {
if err := dircopy.Copy(sourceDir, targetDir); err != nil {
return fmt.Errorf("failure migrating plugindir dir %s into %s: %w", sourceDir, targetDir, err)
}
}
sourceDir = filepath.Join(anrDir, config.NodeConfigs[i].Name, "configs", "chains")
targetDir = filepath.Join(networkDir, node.NodeID.String(), "configs", "chains")
if sdkutils.DirExists(sourceDir) {
if err := dircopy.Copy(sourceDir, targetDir); err != nil {
return fmt.Errorf("failure migrating chain configs dir %s into %s: %w", sourceDir, targetDir, err)
}
}
}
return os.RemoveAll(anrDir)
}