mirror of
https://github.com/luxfi/cli.git
synced 2026-10-30 17:01:20 +00:00
1188 lines
33 KiB
Go
1188 lines
33 KiB
Go
// Copyright (C) 2025, Lux Industries, Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
package localnet
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/netip"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/luxfi/cli/v2/pkg/constants"
|
|
"github.com/luxfi/cli/v2/pkg/utils"
|
|
sdkutils "github.com/luxfi/cli/v2/sdk/utils"
|
|
"github.com/luxfi/node/v2/v2/api/admin"
|
|
"github.com/luxfi/node/v2/v2/api/info"
|
|
"github.com/luxfi/node/v2/v2/config"
|
|
"github.com/luxfi/node/v2/v2/config/node"
|
|
"github.com/luxfi/node/v2/v2/genesis"
|
|
"github.com/luxfi/ids"
|
|
"github.com/luxfi/node/v2/v2/tests/fixture/tmpnet"
|
|
luxdconstants "github.com/luxfi/node/v2/v2/utils/constants"
|
|
"github.com/luxfi/node/v2/v2/utils/logging"
|
|
"github.com/luxfi/node/v2/v2/utils/set"
|
|
"github.com/luxfi/node/v2/v2/vms/platformvm"
|
|
"github.com/luxfi/node/v2/v2/vms/platformvm/txs"
|
|
"github.com/luxfi/node/v2/v2/wallet/subnet/primary"
|
|
walletoptions "github.com/luxfi/node/v2/v2/wallet"
|
|
|
|
dircopy "github.com/otiai10/copy"
|
|
"golang.org/x/exp/maps"
|
|
)
|
|
|
|
type RunningStatus int64
|
|
|
|
const (
|
|
UndefinedRunningStatus RunningStatus = iota
|
|
NotRunning // no network node is running
|
|
PartiallyRunning // only part of the network nodes are running
|
|
Running // all network nodes are running
|
|
)
|
|
|
|
type NodeSetting struct {
|
|
StakingTLSKey []byte
|
|
StakingCertKey []byte
|
|
StakingSignerKey []byte
|
|
HTTPPort uint64
|
|
StakingPort uint64
|
|
}
|
|
|
|
// Creates a new tmpnet with the given parameters
|
|
// Accepts:
|
|
// - setting specific [networkDir] for the network,
|
|
// - a list of [nodes] where some of them have pregenerated parameters
|
|
// - [genesis] and [upgradeBytes]
|
|
// - [bootstrapIPs] and [bootstrapIDs] to be used (if bootstrapping from another custom network)
|
|
// - can be bootstrapped or not depending on [bootstrap] setting
|
|
func TmpNetCreate(
|
|
ctx context.Context,
|
|
log logging.Logger,
|
|
networkDir string,
|
|
luxdBinPath string,
|
|
pluginDir string,
|
|
networkID uint32,
|
|
bootstrapIPs []string,
|
|
bootstrapIDs []string,
|
|
genesis *genesis.UnparsedConfig,
|
|
upgradeBytes []byte,
|
|
defaultFlags map[string]interface{},
|
|
nodes []*tmpnet.Node,
|
|
bootstrap bool,
|
|
) (*tmpnet.Network, error) {
|
|
if len(upgradeBytes) > 0 {
|
|
defaultFlags[config.UpgradeFileContentKey] = base64.StdEncoding.EncodeToString(upgradeBytes)
|
|
}
|
|
network := &tmpnet.Network{
|
|
Nodes: nodes,
|
|
Dir: networkDir,
|
|
DefaultFlags: defaultFlags,
|
|
Genesis: genesis,
|
|
NetworkID: networkID,
|
|
}
|
|
if err := network.EnsureDefaultConfig(log, luxdBinPath, pluginDir); err != nil {
|
|
return nil, err
|
|
}
|
|
if len(bootstrapIPs) > 0 {
|
|
for _, node := range network.Nodes {
|
|
node.SetNetworkingConfig(bootstrapIDs, bootstrapIPs)
|
|
}
|
|
}
|
|
if err := tmpNetSetBlockchainsConfigDir(network); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := network.Write(); err != nil {
|
|
return nil, err
|
|
}
|
|
var err error
|
|
if bootstrap {
|
|
err = TmpNetBootstrap(ctx, log, networkDir)
|
|
}
|
|
return network, err
|
|
}
|
|
|
|
// Copies a tmpnet from [oldDir] to [newDir], fixing
|
|
// configuration so the new network can be bootstrapped
|
|
func TmpNetMove(
|
|
oldDir string,
|
|
newDir string,
|
|
) error {
|
|
if err := dircopy.Copy(oldDir, newDir); err != nil {
|
|
return fmt.Errorf("failure storing network at %s onto %s: %w", oldDir, newDir, err)
|
|
}
|
|
entries, err := os.ReadDir(newDir)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read config dir %s: %w", newDir, err)
|
|
}
|
|
for _, entry := range entries {
|
|
if !entry.IsDir() {
|
|
continue
|
|
}
|
|
flagsFile := filepath.Join(newDir, entry.Name(), "flags.json")
|
|
if utils.FileExists(flagsFile) {
|
|
data, err := utils.ReadJSON(flagsFile)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
data[config.DataDirKey] = filepath.Join(newDir, entry.Name())
|
|
data[config.ChainConfigDirKey], err = tmpNetGetNodeBlockchainConfigsDir(newDir, entry.Name())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, ok := data[config.SubnetConfigDirKey]; ok {
|
|
data[config.SubnetConfigDirKey] = filepath.Join(newDir, "subnets")
|
|
}
|
|
if _, ok := data[config.GenesisFileKey]; ok {
|
|
data[config.GenesisFileKey] = filepath.Join(newDir, "genesis.json")
|
|
}
|
|
if err := utils.WriteJSON(flagsFile, data); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Reads in a tmpnet
|
|
func GetTmpNetNetwork(networkDir string) (*tmpnet.Network, error) {
|
|
network, err := tmpnet.ReadNetwork(networkDir)
|
|
if err != nil {
|
|
return network, err
|
|
}
|
|
for i := range network.Nodes {
|
|
// ensure that URI and StakingAddress are empty if the process does not exists
|
|
processPath := filepath.Join(networkDir, network.Nodes[i].NodeID.String(), "process.json")
|
|
if bytes, err := os.ReadFile(processPath); errors.Is(err, os.ErrNotExist) {
|
|
network.Nodes[i].URI = ""
|
|
network.Nodes[i].StakingAddress = netip.AddrPort{}
|
|
} else if err != nil {
|
|
return network, fmt.Errorf("failed to read node process context: %w", err)
|
|
} else {
|
|
processContext := node.ProcessContext{}
|
|
if err := json.Unmarshal(bytes, &processContext); err != nil {
|
|
return network, fmt.Errorf("failed to unmarshal node process context: %w", err)
|
|
}
|
|
if _, err := utils.GetProcess(processContext.PID); err != nil {
|
|
network.Nodes[i].URI = ""
|
|
network.Nodes[i].StakingAddress = netip.AddrPort{}
|
|
if err := os.Remove(processPath); err != nil {
|
|
return network, fmt.Errorf("failed to clean up node process context: %w", err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
networkID, err := GetTmpNetNetworkID(network)
|
|
if err != nil {
|
|
return network, err
|
|
}
|
|
if IsPublicNetwork(networkID) {
|
|
// this is loaded non empty for public networks, and causes genesis flag to be set later on
|
|
network.Genesis = nil
|
|
}
|
|
return network, nil
|
|
}
|
|
|
|
// Bootstrap a previously generated network
|
|
// If [luxdBinPath] is given, uses it instead of the persisted one
|
|
func TmpNetLoad(
|
|
ctx context.Context,
|
|
log logging.Logger,
|
|
networkDir string,
|
|
luxdBinPath string,
|
|
) (*tmpnet.Network, error) {
|
|
network, err := GetTmpNetNetwork(networkDir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if luxdBinPath != "" {
|
|
for i := range network.Nodes {
|
|
network.Nodes[i].RuntimeConfig = &tmpnet.NodeRuntimeConfig{
|
|
LuxdPath: luxdBinPath,
|
|
}
|
|
}
|
|
}
|
|
if err := network.Write(); err != nil {
|
|
return nil, err
|
|
}
|
|
err = TmpNetBootstrap(ctx, log, networkDir)
|
|
return network, err
|
|
}
|
|
|
|
// Stops the given network
|
|
func TmpNetStop(
|
|
networkDir string,
|
|
) error {
|
|
network, err := GetTmpNetNetwork(networkDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ctx, cancel := sdkutils.GetTimedContext(2 * time.Minute)
|
|
defer cancel()
|
|
var errs []error
|
|
// Initiate stop on all nodes
|
|
for _, node := range network.Nodes {
|
|
node.URI = "" // avoid saving metrics snapshot
|
|
if err := node.InitiateStop(ctx); err != nil {
|
|
errs = append(errs, fmt.Errorf("failed to stop node %s: %w", node.NodeID, err))
|
|
}
|
|
}
|
|
// Wait for stop to complete on all nodes
|
|
for _, node := range network.Nodes {
|
|
if err := node.WaitForStopped(ctx); err != nil {
|
|
errs = append(errs, fmt.Errorf("failed to wait for node %s to stop: %w", node.NodeID, err))
|
|
}
|
|
}
|
|
if len(errs) > 0 {
|
|
return fmt.Errorf("failed to stop network:\n%w", errors.Join(errs...))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Indicates whether the given network has all, part, or none of its nodes running
|
|
func GetTmpNetRunningStatus(networkDir string) (RunningStatus, error) {
|
|
status := UndefinedRunningStatus
|
|
network, err := GetTmpNetNetwork(networkDir)
|
|
if err != nil {
|
|
return status, err
|
|
}
|
|
bootstrappedCount := 0
|
|
for _, node := range network.Nodes {
|
|
// tmpnet.ReadNetwork reads the process state of the nodes and ensures the
|
|
// node.URI field is populated only if the node is running
|
|
if len(node.URI) > 0 {
|
|
bootstrappedCount++
|
|
}
|
|
}
|
|
switch bootstrappedCount {
|
|
case 0:
|
|
return NotRunning, nil
|
|
case len(network.Nodes):
|
|
return Running, nil
|
|
default:
|
|
return status, nil
|
|
}
|
|
}
|
|
|
|
// Get first node of the network
|
|
func GetTmpNetFirstNode(network *tmpnet.Network) (*tmpnet.Node, error) {
|
|
for _, node := range network.Nodes {
|
|
return node, nil
|
|
}
|
|
return nil, fmt.Errorf("no node found on local network at %s", network.Dir)
|
|
}
|
|
|
|
// Get first running node of the network
|
|
func GetTmpNetFirstRunningNode(network *tmpnet.Network) (*tmpnet.Node, error) {
|
|
for _, node := range network.Nodes {
|
|
if node.StakingAddress != (netip.AddrPort{}) {
|
|
return node, nil
|
|
}
|
|
}
|
|
return nil, fmt.Errorf("no running node found on local network at %s", network.Dir)
|
|
}
|
|
|
|
// Get a endpoint to operate with the network
|
|
func GetTmpNetEndpoint(network *tmpnet.Network) (string, error) {
|
|
node, err := GetTmpNetFirstRunningNode(network)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return node.URI, nil
|
|
}
|
|
|
|
// Waits for the given blockchain to be bootstrapped on network
|
|
// Check this for all network nodes that are also validators of the subnet
|
|
// If the network does not validate the blockchain at all, it errors
|
|
func WaitTmpNetBlockchainBootstrapped(
|
|
ctx context.Context,
|
|
network *tmpnet.Network,
|
|
blockchainID string,
|
|
subnetID ids.ID,
|
|
) error {
|
|
if _, ok := ctx.Deadline(); !ok {
|
|
return fmt.Errorf("no deadline given to a blockchain bootstrapping busy wait. endless loop is possible")
|
|
}
|
|
blockchainBootstrapCheckFrequency := time.Second
|
|
for {
|
|
bootstrapped, err := IsTmpNetBlockchainBootstrapped(ctx, network, blockchainID, subnetID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if bootstrapped {
|
|
break
|
|
}
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
case <-time.After(blockchainBootstrapCheckFrequency):
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Indicates if the given blockchain is bootstrapped on the network
|
|
// Check this for all network nodes that are also trackers of the subnet
|
|
// If the network does not track the blockchain at all, it errors
|
|
func IsTmpNetBlockchainBootstrapped(
|
|
ctx context.Context,
|
|
network *tmpnet.Network,
|
|
blockchainID string,
|
|
subnetID ids.ID,
|
|
) (bool, error) {
|
|
queried := 0
|
|
for _, node := range network.Nodes {
|
|
if isTracking, err := IsTmpNetNodeTrackingSubnet([]*tmpnet.Node{node}, subnetID); err != nil {
|
|
return false, err
|
|
} else if !isTracking {
|
|
continue
|
|
}
|
|
infoClient := info.NewClient(node.URI)
|
|
bootstrapped, err := infoClient.IsBootstrapped(ctx, blockchainID)
|
|
if err != nil && !strings.Contains(err.Error(), "there is no chain with alias/ID") {
|
|
return false, err
|
|
}
|
|
if !bootstrapped {
|
|
return false, nil
|
|
}
|
|
queried++
|
|
}
|
|
if queried == 0 {
|
|
return false, fmt.Errorf("no trackers of %s present on network at %s", blockchainID, network.Dir)
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
// Indicates if any of the [nodes] do track [subnetID]
|
|
func IsTmpNetNodeTrackingSubnet(
|
|
nodes []*tmpnet.Node,
|
|
subnetID ids.ID,
|
|
) (bool, error) {
|
|
if subnetID == ids.Empty {
|
|
return true, nil
|
|
}
|
|
trackedSubnets, err := GetTmpNetTrackedSubnets(nodes)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return sdkutils.Belongs(trackedSubnets, subnetID), nil
|
|
}
|
|
|
|
// Returns the subnets tracked by [nodes]
|
|
func GetTmpNetTrackedSubnets(
|
|
nodes []*tmpnet.Node,
|
|
) ([]ids.ID, error) {
|
|
trackedSubnets := []ids.ID{}
|
|
for _, node := range nodes {
|
|
subnets, err := node.Flags.GetStringVal(config.TrackSubnetsKey)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failure obtaining tracked subnets flag of node %s: %w", node.NodeID, err)
|
|
}
|
|
subnets = strings.TrimSpace(subnets)
|
|
if subnets != "" {
|
|
for _, subnetStr := range strings.Split(subnets, ",") {
|
|
subnet, err := ids.FromString(subnetStr)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failure parsing subnet ID from tracked subnet %s of node %s: %w", subnetStr, node.NodeID, err)
|
|
}
|
|
if !sdkutils.Belongs(trackedSubnets, subnet) {
|
|
trackedSubnets = append(trackedSubnets, subnet)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return trackedSubnets, nil
|
|
}
|
|
|
|
// Assign alias [alias]->[blockchainID] to the given [nodes] of [network]
|
|
// if none of the nodes validate the blockchain, it errors
|
|
func TmpNetSetAlias(
|
|
nodes []*tmpnet.Node,
|
|
blockchainID string,
|
|
alias string,
|
|
subnetID ids.ID,
|
|
) error {
|
|
for _, node := range nodes {
|
|
if isTracking, err := IsTmpNetNodeTrackingSubnet([]*tmpnet.Node{node}, subnetID); err != nil {
|
|
return err
|
|
} else if !isTracking {
|
|
continue
|
|
}
|
|
adminClient := admin.NewClient(node.URI)
|
|
ctx, cancel := sdkutils.GetAPIContext()
|
|
defer cancel()
|
|
aliases, err := adminClient.GetChainAliases(ctx, blockchainID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !sdkutils.Belongs(aliases, alias) {
|
|
if err := adminClient.AliasChain(ctx, blockchainID, alias); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Assign alias [blockchain.Name]->[blockchain.ID] for all non standard
|
|
// blockchains on the [network]
|
|
// if the blockchain is not tracked by the network, skips it
|
|
func TmpNetSetDefaultAliases(ctx context.Context, networkDir string) error {
|
|
network, err := GetTmpNetNetwork(networkDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := WaitTmpNetBlockchainBootstrapped(ctx, network, "P", ids.Empty); err != nil {
|
|
return err
|
|
}
|
|
endpoint, err := GetTmpNetEndpoint(network)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
blockchains, err := GetBlockchainsInfo(endpoint)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, blockchain := range blockchains {
|
|
if tracking, err := IsTmpNetNodeTrackingSubnet(network.Nodes, blockchain.SubnetID); err != nil {
|
|
return err
|
|
} else if !tracking {
|
|
continue
|
|
}
|
|
if err := WaitTmpNetBlockchainBootstrapped(ctx, network, blockchain.ID.String(), blockchain.SubnetID); err != nil {
|
|
return err
|
|
}
|
|
if err := TmpNetSetAlias(network.Nodes, blockchain.ID.String(), blockchain.Name, blockchain.SubnetID); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Install the given VM binary into the appropriate location with the
|
|
// appropriate name
|
|
func TmpNetInstallVM(
|
|
log logging.Logger,
|
|
network *tmpnet.Network,
|
|
binaryPath string,
|
|
vmID ids.ID,
|
|
) error {
|
|
pluginDir, err := network.DefaultFlags.GetStringVal(config.PluginDirKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
pluginPath := filepath.Join(pluginDir, vmID.String())
|
|
return utils.SetupExecFile(log, binaryPath, pluginPath)
|
|
}
|
|
|
|
// Set up blockchain config for the given [nodes] of [network]
|
|
func TmpNetSetBlockchainConfig(
|
|
network *tmpnet.Network,
|
|
nodes []*tmpnet.Node,
|
|
blockchainID ids.ID,
|
|
blockchainConfig []byte,
|
|
blockchainUpgrades []byte,
|
|
) error {
|
|
if err := tmpNetSetBlockchainsConfigDir(network); err != nil {
|
|
return err
|
|
}
|
|
for _, node := range nodes {
|
|
if err := TmpNetSetNodeBlockchainConfig(
|
|
network,
|
|
node.NodeID,
|
|
blockchainID,
|
|
blockchainConfig,
|
|
blockchainUpgrades,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Set up blockchain config for the given [nodeID] of the [network]
|
|
func TmpNetSetNodeBlockchainConfig(
|
|
network *tmpnet.Network,
|
|
nodeID ids.NodeID,
|
|
blockchainID ids.ID,
|
|
blockchainConfig []byte,
|
|
blockchainUpgrades []byte,
|
|
) error {
|
|
var configPath, upgradesPath string
|
|
for _, node := range network.Nodes {
|
|
if node.NodeID != nodeID {
|
|
continue
|
|
}
|
|
blockchainsConfigDir, err := node.Flags.GetStringVal(config.ChainConfigDirKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
configPath = filepath.Join(
|
|
blockchainsConfigDir,
|
|
blockchainID.String(),
|
|
"config.json",
|
|
)
|
|
upgradesPath = filepath.Join(
|
|
blockchainsConfigDir,
|
|
blockchainID.String(),
|
|
"upgrade.json",
|
|
)
|
|
configDir := filepath.Dir(configPath)
|
|
if err := os.MkdirAll(configDir, constants.DefaultPerms755); err != nil {
|
|
return fmt.Errorf("could not create blockchain config directory %s: %w", configDir, err)
|
|
}
|
|
}
|
|
if configPath == "" {
|
|
return fmt.Errorf("failure writing chain config file: node %s not found on network", nodeID)
|
|
}
|
|
if blockchainConfig != nil {
|
|
if err := os.WriteFile(configPath, blockchainConfig, constants.WriteReadReadPerms); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if blockchainUpgrades != nil {
|
|
if err := os.WriteFile(upgradesPath, blockchainUpgrades, constants.WriteReadReadPerms); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Return path to the blockchain configs dir for the given [networkDir] and [nodeID]. If the dir does not
|
|
// exists, first creates it.
|
|
func tmpNetGetNodeBlockchainConfigsDir(networkDir string, nodeID string) (string, error) {
|
|
nodeBlockchainConfigsDir := filepath.Join(networkDir, nodeID, "configs", "chains")
|
|
if err := os.MkdirAll(nodeBlockchainConfigsDir, constants.DefaultPerms755); err != nil {
|
|
return "", fmt.Errorf("could not create node blockchains config directory %s: %w", nodeBlockchainConfigsDir, err)
|
|
}
|
|
return nodeBlockchainConfigsDir, nil
|
|
}
|
|
|
|
// Set up the blockchain configs dir for all nodes in the [network]
|
|
func tmpNetSetBlockchainsConfigDir(network *tmpnet.Network) error {
|
|
for _, node := range network.Nodes {
|
|
nodeBlockchainConfigsDir, err := tmpNetGetNodeBlockchainConfigsDir(network.Dir, node.NodeID.String())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
node.Flags[config.ChainConfigDirKey] = nodeBlockchainConfigsDir
|
|
if err := node.Write(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Set up subnet config for all nodes in the network
|
|
func TmpNetSetSubnetConfig(
|
|
network *tmpnet.Network,
|
|
subnetID ids.ID,
|
|
subnetConfig []byte,
|
|
) error {
|
|
subnetConfigsDir := filepath.Join(network.Dir, "subnets")
|
|
for _, node := range network.Nodes {
|
|
node.Flags[config.SubnetConfigDirKey] = subnetConfigsDir
|
|
if err := node.Write(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
configPath := filepath.Join(
|
|
subnetConfigsDir,
|
|
subnetID.String()+".json",
|
|
)
|
|
configDir := filepath.Dir(configPath)
|
|
if err := os.MkdirAll(configDir, constants.DefaultPerms755); err != nil {
|
|
return fmt.Errorf("could not create subnets config directory %s: %w", configDir, err)
|
|
}
|
|
return os.WriteFile(configPath, subnetConfig, constants.WriteReadReadPerms)
|
|
}
|
|
|
|
// Restart given [nodes] of [network]
|
|
// If [subnetIDs] are given, configure the nodes to track the subnets
|
|
func TmpNetRestartNodes(
|
|
ctx context.Context,
|
|
log logging.Logger,
|
|
printFunc func(msg string, args ...interface{}),
|
|
network *tmpnet.Network,
|
|
nodes []*tmpnet.Node,
|
|
subnetIDs []ids.ID,
|
|
) error {
|
|
for _, node := range nodes {
|
|
if len(subnetIDs) > 0 {
|
|
printFunc("Restarting node %s to track newly deployed subnet/s", node.NodeID)
|
|
subnets, err := node.Flags.GetStringVal(config.TrackSubnetsKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
subnetsSet := set.Set[string]{}
|
|
subnets = strings.TrimSpace(subnets)
|
|
if subnets != "" {
|
|
subnetsSet = set.Of(strings.Split(subnets, ",")...)
|
|
}
|
|
for _, subnetID := range subnetIDs {
|
|
subnetsSet.Add(subnetID.String())
|
|
}
|
|
subnets = strings.Join(subnetsSet.List(), ",")
|
|
node.Flags[config.TrackSubnetsKey] = subnets
|
|
}
|
|
if err := TmpNetRestartNode(ctx, log, network, node); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return WaitTmpNetBlockchainBootstrapped(ctx, network, "P", ids.Empty)
|
|
}
|
|
|
|
// Get network bootstrappers to use to connect to the network
|
|
func GetTmpNetBootstrappers(
|
|
networkDir string,
|
|
skipNodeID ids.NodeID,
|
|
) ([]string, []string, error) {
|
|
network, err := GetTmpNetNetwork(networkDir)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
bootstrapIPs := []string{}
|
|
bootstrapIDs := []string{}
|
|
for _, node := range network.Nodes {
|
|
if node.NodeID == skipNodeID {
|
|
continue
|
|
}
|
|
if node.StakingAddress == (netip.AddrPort{}) {
|
|
continue
|
|
}
|
|
bootstrapIPs = append(bootstrapIPs, node.StakingAddress.String())
|
|
bootstrapIDs = append(bootstrapIDs, node.NodeID.String())
|
|
}
|
|
return bootstrapIPs, bootstrapIDs, nil
|
|
}
|
|
|
|
// Get network genesis
|
|
func GetTmpNetGenesis(
|
|
networkDir string,
|
|
) ([]byte, error) {
|
|
return os.ReadFile(filepath.Join(networkDir, "genesis.json"))
|
|
}
|
|
|
|
// Get network upgrade
|
|
func GetTmpNetUpgrade(
|
|
networkDir string,
|
|
) ([]byte, error) {
|
|
network, err := GetTmpNetNetwork(networkDir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
encodedUpgrade, err := network.DefaultFlags.GetStringVal(config.UpgradeFileContentKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return base64.StdEncoding.DecodeString(encodedUpgrade)
|
|
}
|
|
|
|
// Restart all nodes on [networkDir] to track [subnetID].
|
|
// If [wallet] is given, for non [sovereign] flows, add nodes as non sovereign validators
|
|
// Waits until both P-Chain and the blockchain [blockchainID] are bootstrapped
|
|
func TmpNetTrackSubnet(
|
|
ctx context.Context,
|
|
log logging.Logger,
|
|
printFunc func(msg string, args ...interface{}),
|
|
networkDir string,
|
|
sovereign bool,
|
|
blockchainID ids.ID,
|
|
subnetID ids.ID,
|
|
wallet *primary.Wallet,
|
|
) error {
|
|
network, err := GetTmpNetNetwork(networkDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Restart nodes
|
|
if err := TmpNetRestartNodes(
|
|
ctx,
|
|
log,
|
|
printFunc,
|
|
network,
|
|
network.Nodes,
|
|
[]ids.ID{subnetID},
|
|
); err != nil {
|
|
return err
|
|
}
|
|
if err := WaitTmpNetBlockchainBootstrapped(ctx, network, "P", ids.Empty); err != nil {
|
|
return err
|
|
}
|
|
if !sovereign && wallet != nil {
|
|
if err := TmpNetAddNonSovereignValidators(ctx, network, subnetID, wallet); err != nil {
|
|
return err
|
|
}
|
|
if err := TmpNetWaitNonSovereignValidators(ctx, network, subnetID); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
printFunc("Waiting for blockchain %s to be bootstrapped", blockchainID)
|
|
if err := WaitTmpNetBlockchainBootstrapped(ctx, network, blockchainID.String(), subnetID); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Set up blockchain config file from [vmBinaryPath], [blockchainConfig], [perNodeBlockchainConfig], [blockchainUpgrades], [subnetConfig]
|
|
func TmpNetUpdateBlockchainConfig(
|
|
log logging.Logger,
|
|
networkDir string,
|
|
subnetID ids.ID,
|
|
blockchainID ids.ID,
|
|
vmID ids.ID,
|
|
vmBinaryPath string,
|
|
blockchainConfig []byte,
|
|
perNodeBlockchainConfig map[ids.NodeID][]byte,
|
|
blockchainUpgrades []byte,
|
|
subnetConfig []byte,
|
|
nodeConfig map[string]interface{},
|
|
) error {
|
|
network, err := GetTmpNetNetwork(networkDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// blockchain specific luxd flags
|
|
for i := range network.Nodes {
|
|
for k, v := range nodeConfig {
|
|
network.Nodes[i].Flags[k] = v
|
|
}
|
|
}
|
|
// VM Binary setup
|
|
if err := TmpNetInstallVM(log, network, vmBinaryPath, vmID); err != nil {
|
|
return err
|
|
}
|
|
// Configs
|
|
if subnetConfig != nil {
|
|
if err := TmpNetSetSubnetConfig(
|
|
network,
|
|
subnetID,
|
|
subnetConfig,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if blockchainConfig != nil || blockchainUpgrades != nil {
|
|
if err := TmpNetSetBlockchainConfig(
|
|
network,
|
|
network.Nodes,
|
|
blockchainID,
|
|
blockchainConfig,
|
|
blockchainUpgrades,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
nodeIDs := sdkutils.Map(network.Nodes, func(node *tmpnet.Node) ids.NodeID { return node.NodeID })
|
|
for nodeID, blockchainConfig := range perNodeBlockchainConfig {
|
|
if !sdkutils.Belongs(nodeIDs, nodeID) {
|
|
continue
|
|
}
|
|
if err := TmpNetSetNodeBlockchainConfig(
|
|
network,
|
|
nodeID,
|
|
blockchainID,
|
|
blockchainConfig,
|
|
nil,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return network.Write()
|
|
}
|
|
|
|
// Add all network nodes of [network] as non SOV validators of [subnetID], using [wallet] to pay for fees
|
|
// If a node is already validator for the subnet, does nothing with it
|
|
func TmpNetAddNonSovereignValidators(
|
|
ctx context.Context,
|
|
network *tmpnet.Network,
|
|
subnetID ids.ID,
|
|
wallet *primary.Wallet,
|
|
) error {
|
|
endpoint, err := GetTmpNetEndpoint(network)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
pClient := platformvm.NewClient(endpoint)
|
|
vs, err := pClient.GetCurrentValidators(ctx, luxdconstants.PrimaryNetworkID, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
primaryValidatorsEndtime := make(map[ids.NodeID]time.Time)
|
|
for _, v := range vs {
|
|
primaryValidatorsEndtime[v.NodeID] = time.Unix(int64(v.EndTime), 0)
|
|
}
|
|
vs, err = pClient.GetCurrentValidators(ctx, subnetID, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
subnetValidators := set.Set[ids.NodeID]{}
|
|
for _, v := range vs {
|
|
subnetValidators.Add(v.NodeID)
|
|
}
|
|
for _, node := range network.Nodes {
|
|
if isValidator := subnetValidators.Contains(node.NodeID); isValidator {
|
|
continue
|
|
}
|
|
if _, err := wallet.P().IssueAddSubnetValidatorTx(
|
|
&txs.SubnetValidator{
|
|
Validator: txs.Validator{
|
|
NodeID: node.NodeID,
|
|
End: uint64(primaryValidatorsEndtime[node.NodeID].Unix()),
|
|
Wght: 1000,
|
|
},
|
|
Subnet: subnetID,
|
|
},
|
|
walletoptions.WithContext(ctx),
|
|
walletoptions.WithPollFrequency(100*time.Millisecond),
|
|
); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Waits until all the network nodes of [network] are included as validators of [subnetID] as verified
|
|
// on GetCurrentValidators P-Chain API call
|
|
func TmpNetWaitNonSovereignValidators(ctx context.Context, network *tmpnet.Network, subnetID ids.ID) error {
|
|
checkFrequency := time.Second
|
|
endpoint, err := GetTmpNetEndpoint(network)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
pClient := platformvm.NewClient(endpoint)
|
|
for _, node := range network.Nodes {
|
|
for {
|
|
vs, err := pClient.GetCurrentValidators(ctx, subnetID, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
subnetValidators := set.Set[ids.NodeID]{}
|
|
for _, v := range vs {
|
|
subnetValidators.Add(v.NodeID)
|
|
}
|
|
if subnetValidators.Contains(node.NodeID) {
|
|
break
|
|
}
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
case <-time.After(checkFrequency):
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Return a slice of new [numNodes] nodes, setting keys and ports to either fresh values,
|
|
// or values present at companion [nodeSettings] slice
|
|
// If [trackedSubnets] are given, set up appropriate flag
|
|
func GetNewTmpNetNodes(
|
|
numNodes uint32,
|
|
nodeSettings []NodeSetting,
|
|
trackedSubnets []ids.ID,
|
|
) ([]*tmpnet.Node, error) {
|
|
if len(nodeSettings) > int(numNodes) {
|
|
return nil, fmt.Errorf("node settings length is bigger than the number of nodes")
|
|
}
|
|
nodes := []*tmpnet.Node{}
|
|
for i := range numNodes {
|
|
node := tmpnet.NewNode("")
|
|
if int(i) < len(nodeSettings) {
|
|
if len(nodeSettings[i].StakingCertKey) > 0 {
|
|
node.Flags[config.StakingCertContentKey] = base64.StdEncoding.EncodeToString(nodeSettings[i].StakingCertKey)
|
|
}
|
|
if len(nodeSettings[i].StakingTLSKey) > 0 {
|
|
node.Flags[config.StakingTLSKeyContentKey] = base64.StdEncoding.EncodeToString(nodeSettings[i].StakingTLSKey)
|
|
}
|
|
if len(nodeSettings[i].StakingSignerKey) > 0 {
|
|
node.Flags[config.StakingSignerKeyContentKey] = base64.StdEncoding.EncodeToString(nodeSettings[i].StakingSignerKey)
|
|
}
|
|
node.Flags[config.HTTPPortKey] = nodeSettings[i].HTTPPort
|
|
node.Flags[config.StakingPortKey] = nodeSettings[i].StakingPort
|
|
} else {
|
|
node.Flags[config.HTTPPortKey] = 0
|
|
node.Flags[config.StakingPortKey] = 0
|
|
}
|
|
if len(trackedSubnets) > 0 {
|
|
trackedSubnetsStr := sdkutils.Map(trackedSubnets, func(i ids.ID) string { return i.String() })
|
|
node.Flags[config.TrackSubnetsKey] = strings.Join(trackedSubnetsStr, ",")
|
|
}
|
|
if err := node.EnsureKeys(); err != nil {
|
|
return nil, err
|
|
}
|
|
nodes = append(nodes, node)
|
|
}
|
|
return nodes, nil
|
|
}
|
|
|
|
// Copies [node] data into a new fresh node that is to be used in the same network
|
|
// Keeps information regarding genesis, upgrade, bootstrappers, tracked subnets, ...
|
|
func TmpNetCopyNode(
|
|
node *tmpnet.Node,
|
|
) (*tmpnet.Node, error) {
|
|
if node == nil {
|
|
return nil, fmt.Errorf("can't copy nil node")
|
|
}
|
|
flags := maps.Clone(node.Flags)
|
|
for _, flag := range []string{
|
|
config.StakingCertContentKey,
|
|
config.StakingTLSKeyContentKey,
|
|
config.StakingSignerKeyContentKey,
|
|
config.DataDirKey,
|
|
config.HTTPPortKey,
|
|
config.StakingPortKey,
|
|
} {
|
|
delete(flags, flag)
|
|
}
|
|
newNode := tmpnet.Node{
|
|
Flags: flags,
|
|
}
|
|
if err := newNode.EnsureKeys(); err != nil {
|
|
return nil, nil
|
|
}
|
|
return &newNode, nil
|
|
}
|
|
|
|
// Starts all nodes of [networkDir], and waits for P-chain to be bootstrapped
|
|
// Then, persists HTTP and Staking ports (changing the config from dynamic
|
|
// ports -if set to 0- into persisted ones)
|
|
func TmpNetBootstrap(
|
|
ctx context.Context,
|
|
log logging.Logger,
|
|
networkDir string,
|
|
) error {
|
|
network, err := GetTmpNetNetwork(networkDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, node := range network.Nodes {
|
|
if err := TmpNetStartNode(ctx, log, network, node); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if err := WaitTmpNetBlockchainBootstrapped(ctx, network, "P", ids.Empty); err != nil {
|
|
return err
|
|
}
|
|
return TmpNetPersistPorts(network)
|
|
}
|
|
|
|
// Adds the given [node] to the [network] conf, and starts it
|
|
// Waits for P-Chain to be bootstrapped, and persists ports for the node
|
|
func TmpNetAddNode(
|
|
ctx context.Context,
|
|
log logging.Logger,
|
|
network *tmpnet.Network,
|
|
node *tmpnet.Node,
|
|
httpPort uint32,
|
|
stakingPort uint32,
|
|
) error {
|
|
node.Flags[config.HTTPPortKey] = httpPort
|
|
node.Flags[config.StakingPortKey] = stakingPort
|
|
network.Nodes = append(network.Nodes, node)
|
|
if err := network.EnsureNodeConfig(node); err != nil {
|
|
return err
|
|
}
|
|
if err := tmpNetSetBlockchainsConfigDir(network); err != nil {
|
|
return err
|
|
}
|
|
if err := network.Write(); err != nil {
|
|
return err
|
|
}
|
|
if err := TmpNetStartNode(ctx, log, network, node); err != nil {
|
|
return err
|
|
}
|
|
if err := WaitTmpNetBlockchainBootstrapped(ctx, network, "P", ids.Empty); err != nil {
|
|
return err
|
|
}
|
|
return TmpNetPersistPorts(network)
|
|
}
|
|
|
|
// Enables sybil proyection on [networkDir]
|
|
// This is disabled by default on tmpnet for 1-node networks, but is generally
|
|
//
|
|
// needed for 1-node clusters that connect to other network
|
|
func TmpNetEnableSybilProtection(
|
|
networkDir string,
|
|
) error {
|
|
network, err := GetTmpNetNetwork(networkDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
network.DefaultFlags[config.SybilProtectionEnabledKey] = true
|
|
for i := range network.Nodes {
|
|
network.Nodes[i].Flags[config.SybilProtectionEnabledKey] = true
|
|
}
|
|
return network.Write()
|
|
}
|
|
|
|
// Persists http and staking ports of a running network
|
|
func TmpNetPersistPorts(
|
|
network *tmpnet.Network,
|
|
) error {
|
|
for i := range network.Nodes {
|
|
ipPort, err := utils.GetIPPort(network.Nodes[i].URI)
|
|
if err != nil {
|
|
return fmt.Errorf("couldn't parse node URI %s: %w", network.Nodes[i].URI, err)
|
|
}
|
|
network.Nodes[i].Flags[config.HTTPPortKey] = ipPort.Port()
|
|
network.Nodes[i].Flags[config.StakingPortKey] = network.Nodes[i].StakingAddress.Port()
|
|
}
|
|
return network.Write()
|
|
}
|
|
|
|
// Restart given [node] of [network]
|
|
func TmpNetRestartNode(
|
|
ctx context.Context,
|
|
log logging.Logger,
|
|
network *tmpnet.Network,
|
|
node *tmpnet.Node,
|
|
) error {
|
|
if err := node.Stop(ctx); err != nil {
|
|
return fmt.Errorf("failed to stop node %s: %w", node.NodeID, err)
|
|
}
|
|
if err := TmpNetStartNode(ctx, log, network, node); err != nil {
|
|
return fmt.Errorf("failed to start node %s: %w", node.NodeID, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Starts given [node] of [network]
|
|
func TmpNetStartNode(
|
|
ctx context.Context,
|
|
log logging.Logger,
|
|
network *tmpnet.Network,
|
|
node *tmpnet.Node,
|
|
) error {
|
|
networkID, err := GetTmpNetNodeNetworkID(node)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, ok := node.Flags[config.BootstrapIPsKey]
|
|
if !ok && !IsPublicNetwork(networkID) {
|
|
// it does not have boostrappers set, and it is also not a public network node,
|
|
// so we need to set bootstrappers from the custom network itself
|
|
bootstrapIPs, bootstrapIDs, err := GetTmpNetBootstrappers(network.Dir, node.NodeID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
node.SetNetworkingConfig(bootstrapIDs, bootstrapIPs)
|
|
}
|
|
if err := node.Write(); err != nil {
|
|
return err
|
|
}
|
|
if err := node.Start(log); err != nil {
|
|
// Attempt to stop an unhealthy node to provide some assurance to the caller
|
|
// that an error condition will not result in a lingering process.
|
|
return errors.Join(err, node.Stop(ctx))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Indicates wether a given network ID is for public network
|
|
func IsPublicNetwork(networkID uint32) bool {
|
|
return networkID == luxdconstants.TestnetID || networkID == luxdconstants.MainnetID
|
|
}
|
|
|
|
// Returns Network ID of [network]
|
|
// Using this instead of network.GetNetworkID
|
|
// because latest one reads in an empty genesis
|
|
// for public networks on some cases, returning an ID of 0
|
|
func GetTmpNetNetworkID(network *tmpnet.Network) (uint32, error) {
|
|
node, err := GetTmpNetFirstNode(network)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return GetTmpNetNodeNetworkID(node)
|
|
}
|
|
|
|
// Returns Network ID of a [node]
|
|
func GetTmpNetNodeNetworkID(node *tmpnet.Node) (uint32, error) {
|
|
networkIDStr, err := node.Flags.GetStringVal(config.NetworkNameKey)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
networkID, err := strconv.ParseUint(networkIDStr, 10, 32)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return uint32(networkID), nil
|
|
}
|
|
|
|
// Returns luxd path persisted at [networkDir]
|
|
func GetTmpNetLuxdBinaryPath(networkDir string) (string, error) {
|
|
network, err := GetTmpNetNetwork(networkDir)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return network.DefaultRuntimeConfig.LuxdPath, nil
|
|
}
|
|
|
|
// when host is public, we avoid [::] but use public IP
|
|
func fixURI(uri string, ip string) string {
|
|
return strings.Replace(uri, "[::]", ip, 1)
|
|
}
|
|
|
|
// reads in tmpnet for external reference. preferred over tmpnet version due to URI transformation
|
|
func GetTmpNetNetworkWithURIFix(networkDir string) (*tmpnet.Network, error) {
|
|
network, err := GetTmpNetNetwork(networkDir)
|
|
if err != nil {
|
|
return network, err
|
|
}
|
|
for _, node := range network.Nodes {
|
|
nodeIP, err := node.Flags.GetStringVal(config.PublicIPKey)
|
|
if err != nil {
|
|
return network, err
|
|
}
|
|
node.URI = fixURI(node.URI, nodeIP)
|
|
}
|
|
return network, nil
|
|
}
|
|
|
|
// Get all node URIs of the network. transformates URIs
|
|
func GetTmpNetNodeURIsWithFix(
|
|
networkDir string,
|
|
) ([]string, error) {
|
|
network, err := GetTmpNetNetworkWithURIFix(networkDir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return sdkutils.Map(network.GetNodeURIs(), func(nodeURI tmpnet.NodeURI) string { return nodeURI.URI }), nil
|
|
}
|
|
|
|
// Get paths for most important luxd logs that are present on the network nodes
|
|
func GetTmpNetAvailableLogs(
|
|
networkDir string,
|
|
blockchainID ids.ID,
|
|
includeCChain bool,
|
|
) ([]string, error) {
|
|
network, err := GetTmpNetNetwork(networkDir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
prefixes := []string{}
|
|
if blockchainID != ids.Empty {
|
|
prefixes = append(prefixes, blockchainID.String())
|
|
}
|
|
if includeCChain {
|
|
prefixes = append(prefixes, "C")
|
|
}
|
|
prefixes = append(prefixes, "P")
|
|
prefixes = append(prefixes, "main")
|
|
logPaths := []string{}
|
|
for _, node := range network.Nodes {
|
|
for _, prefix := range prefixes {
|
|
logPath := filepath.Join(networkDir, node.NodeID.String(), "logs", prefix+".log")
|
|
if utils.FileExists(logPath) {
|
|
logPaths = append(logPaths, utils.ReplaceUserHomeWithTilde(logPath))
|
|
}
|
|
}
|
|
}
|
|
return logPaths, nil
|
|
}
|