mirror of
https://github.com/luxfi/cli.git
synced 2026-10-30 17:01:20 +00:00
831 lines
26 KiB
Go
831 lines
26 KiB
Go
// Copyright (C) 2022, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
package subnet
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"math/big"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"golang.org/x/exp/maps"
|
|
|
|
"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/models"
|
|
"github.com/luxfi/cli/v2/pkg/utils"
|
|
"github.com/luxfi/cli/v2/pkg/ux"
|
|
"github.com/luxfi/cli/v2/pkg/vm"
|
|
"github.com/luxfi/netrunner/client"
|
|
"github.com/luxfi/netrunner/rpcpb"
|
|
"github.com/luxfi/netrunner/server"
|
|
anrutils "github.com/luxfi/netrunner/utils"
|
|
"github.com/luxfi/node/v2/v2/genesis"
|
|
"github.com/luxfi/ids"
|
|
"github.com/luxfi/node/v2/v2/utils/crypto/keychain"
|
|
"github.com/luxfi/node/v2/v2/utils/storage"
|
|
"github.com/luxfi/node/v2/v2/vms/components/lux"
|
|
"github.com/luxfi/node/v2/v2/vms/components/verify"
|
|
"github.com/luxfi/node/v2/v2/vms/platformvm"
|
|
platformvmapi "github.com/luxfi/node/v2/v2/vms/platformvm/api"
|
|
"github.com/luxfi/node/v2/v2/vms/platformvm/reward"
|
|
"github.com/luxfi/node/v2/v2/vms/platformvm/signer"
|
|
"github.com/luxfi/node/v2/v2/vms/platformvm/txs"
|
|
"github.com/luxfi/node/v2/v2/vms/secp256k1fx"
|
|
"github.com/luxfi/node/v2/v2/wallet/chain/c"
|
|
"github.com/luxfi/node/v2/v2/wallet/subnet/primary"
|
|
"github.com/luxfi/geth/params"
|
|
"github.com/luxfi/evm/v2/core"
|
|
"github.com/luxfi/geth/common"
|
|
"github.com/luxfi/node/v2/v2/utils/set"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
const (
|
|
WriteReadReadPerms = 0o644
|
|
)
|
|
|
|
// emptyEthKeychain is a minimal implementation of EthKeychain for cases where ETH keys are not needed
|
|
type emptyEthKeychain struct{}
|
|
|
|
func (e *emptyEthKeychain) GetEth(addr common.Address) (keychain.Signer, bool) {
|
|
return nil, false
|
|
}
|
|
|
|
func (e *emptyEthKeychain) EthAddresses() set.Set[common.Address] {
|
|
return set.NewSet[common.Address](0)
|
|
}
|
|
|
|
type LocalDeployer struct {
|
|
procChecker binutils.ProcessChecker
|
|
binChecker binutils.BinaryChecker
|
|
getClientFunc getGRPCClientFunc
|
|
binaryDownloader binutils.PluginBinaryDownloader
|
|
app *application.Lux
|
|
backendStartedHere bool
|
|
setDefaultSnapshot setDefaultSnapshotFunc
|
|
luxVersion string
|
|
vmBin string
|
|
}
|
|
|
|
func NewLocalDeployer(app *application.Lux, luxVersion string, vmBin string) *LocalDeployer {
|
|
return &LocalDeployer{
|
|
procChecker: binutils.NewProcessChecker(),
|
|
binChecker: binutils.NewBinaryChecker(),
|
|
getClientFunc: binutils.NewGRPCClient,
|
|
binaryDownloader: binutils.NewPluginBinaryDownloader(app),
|
|
app: app,
|
|
setDefaultSnapshot: SetDefaultSnapshot,
|
|
luxVersion: luxVersion,
|
|
vmBin: vmBin,
|
|
}
|
|
}
|
|
|
|
type getGRPCClientFunc func(...binutils.GRPCClientOpOption) (client.Client, error)
|
|
|
|
type setDefaultSnapshotFunc func(string, bool) error
|
|
|
|
// DeployToLocalNetwork does the heavy lifting:
|
|
// * it checks the gRPC is running, if not, it starts it
|
|
// * kicks off the actual deployment
|
|
func (d *LocalDeployer) DeployToLocalNetwork(chain string, chainGenesis []byte, genesisPath string) (ids.ID, ids.ID, error) {
|
|
if err := d.StartServer(); err != nil {
|
|
return ids.Empty, ids.Empty, err
|
|
}
|
|
return d.doDeploy(chain, chainGenesis, genesisPath)
|
|
}
|
|
|
|
func getAssetID(wallet primary.Wallet, tokenName string, tokenSymbol string, maxSupply uint64) (ids.ID, error) {
|
|
xWallet := wallet.X()
|
|
owner := &secp256k1fx.OutputOwners{
|
|
Threshold: 1,
|
|
Addrs: []ids.ShortID{
|
|
genesis.EWOQKey.PublicKey().Address(),
|
|
},
|
|
}
|
|
_, cancel := context.WithTimeout(context.Background(), constants.DefaultWalletCreationTimeout)
|
|
subnetAssetTx, err := xWallet.IssueCreateAssetTx(
|
|
tokenName,
|
|
tokenSymbol,
|
|
9, // denomination for UI purposes only in explorer
|
|
map[uint32][]verify.State{
|
|
0: {
|
|
&secp256k1fx.TransferOutput{
|
|
Amt: maxSupply,
|
|
OutputOwners: *owner,
|
|
},
|
|
},
|
|
},
|
|
)
|
|
defer cancel()
|
|
if err != nil {
|
|
return ids.Empty, err
|
|
}
|
|
return subnetAssetTx.ID(), nil
|
|
}
|
|
|
|
func exportToPChain(wallet primary.Wallet, owner *secp256k1fx.OutputOwners, subnetAssetID ids.ID, maxSupply uint64) error {
|
|
xWallet := wallet.X()
|
|
_, cancel := context.WithTimeout(context.Background(), constants.DefaultWalletCreationTimeout)
|
|
|
|
_, err := xWallet.IssueExportTx(
|
|
ids.Empty,
|
|
[]*lux.TransferableOutput{
|
|
{
|
|
Asset: lux.Asset{
|
|
ID: subnetAssetID,
|
|
},
|
|
Out: &secp256k1fx.TransferOutput{
|
|
Amt: maxSupply,
|
|
OutputOwners: *owner,
|
|
},
|
|
},
|
|
},
|
|
)
|
|
defer cancel()
|
|
return err
|
|
}
|
|
|
|
func importFromXChain(wallet primary.Wallet, owner *secp256k1fx.OutputOwners) error {
|
|
pWallet := wallet.P()
|
|
xChainID := ids.FromStringOrPanic("2oYMBNV4eNHyqk2fjjV5nVQLDbtmNJzq5s3qs3Lo6ftnC6FByM") // X-Chain ID
|
|
_, cancel := context.WithTimeout(context.Background(), constants.DefaultWalletCreationTimeout)
|
|
_, err := pWallet.IssueImportTx(
|
|
xChainID,
|
|
owner,
|
|
)
|
|
defer cancel()
|
|
return err
|
|
}
|
|
|
|
func IssueTransformSubnetTx(
|
|
elasticSubnetConfig models.ElasticSubnetConfig,
|
|
kc keychain.Keychain,
|
|
subnetID ids.ID,
|
|
tokenName string,
|
|
tokenSymbol string,
|
|
maxSupply uint64,
|
|
) (ids.ID, ids.ID, error) {
|
|
ctx := context.Background()
|
|
api := constants.LocalAPIEndpoint
|
|
// Create empty EthKeychain if kc doesn't implement it
|
|
var ethKc c.EthKeychain
|
|
if ekc, ok := kc.(c.EthKeychain); ok {
|
|
ethKc = ekc
|
|
} else {
|
|
// Create a minimal EthKeychain implementation
|
|
ethKc = &emptyEthKeychain{}
|
|
}
|
|
wallet, err := primary.MakeWallet(ctx, api, kc, ethKc, primary.WalletConfig{})
|
|
if err != nil {
|
|
return ids.Empty, ids.Empty, err
|
|
}
|
|
subnetAssetID, err := getAssetID(*wallet, tokenName, tokenSymbol, maxSupply)
|
|
if err != nil {
|
|
return ids.Empty, ids.Empty, err
|
|
}
|
|
owner := &secp256k1fx.OutputOwners{
|
|
Threshold: 1,
|
|
Addrs: []ids.ShortID{
|
|
genesis.EWOQKey.PublicKey().Address(),
|
|
},
|
|
}
|
|
err = exportToPChain(*wallet, owner, subnetAssetID, maxSupply)
|
|
if err != nil {
|
|
return ids.Empty, ids.Empty, err
|
|
}
|
|
err = importFromXChain(*wallet, owner)
|
|
if err != nil {
|
|
return ids.Empty, ids.Empty, err
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), constants.DefaultConfirmTxTimeout)
|
|
transformSubnetTxID, err := wallet.P().IssueTransformSubnetTx(elasticSubnetConfig.SubnetID, subnetAssetID,
|
|
elasticSubnetConfig.InitialSupply, elasticSubnetConfig.MaxSupply, elasticSubnetConfig.MinConsumptionRate,
|
|
elasticSubnetConfig.MaxConsumptionRate, elasticSubnetConfig.MinValidatorStake, elasticSubnetConfig.MaxValidatorStake,
|
|
elasticSubnetConfig.MinStakeDuration, elasticSubnetConfig.MaxStakeDuration, elasticSubnetConfig.MinDelegationFee,
|
|
elasticSubnetConfig.MinDelegatorStake, elasticSubnetConfig.MaxValidatorWeightFactor, elasticSubnetConfig.UptimeRequirement,
|
|
)
|
|
defer cancel()
|
|
if err != nil {
|
|
return ids.Empty, ids.Empty, err
|
|
}
|
|
return transformSubnetTxID.ID(), subnetAssetID, err
|
|
}
|
|
|
|
func IssueAddPermissionlessValidatorTx(
|
|
kc keychain.Keychain,
|
|
subnetID ids.ID,
|
|
nodeID ids.NodeID,
|
|
stakeAmount uint64,
|
|
assetID ids.ID,
|
|
startTime uint64,
|
|
endTime uint64,
|
|
) (ids.ID, error) {
|
|
ctx := context.Background()
|
|
api := constants.LocalAPIEndpoint
|
|
// Create empty EthKeychain if kc doesn't implement it
|
|
var ethKc c.EthKeychain
|
|
if ekc, ok := kc.(c.EthKeychain); ok {
|
|
ethKc = ekc
|
|
} else {
|
|
// Create a minimal EthKeychain implementation
|
|
ethKc = &emptyEthKeychain{}
|
|
}
|
|
wallet, err := primary.MakeWallet(ctx, api, kc, ethKc, primary.WalletConfig{})
|
|
if err != nil {
|
|
return ids.Empty, err
|
|
}
|
|
owner := &secp256k1fx.OutputOwners{
|
|
Threshold: 1,
|
|
Addrs: []ids.ShortID{
|
|
genesis.EWOQKey.PublicKey().Address(),
|
|
},
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), constants.DefaultConfirmTxTimeout)
|
|
txID, err := wallet.P().IssueAddPermissionlessValidatorTx(
|
|
&txs.SubnetValidator{
|
|
Validator: txs.Validator{
|
|
NodeID: nodeID,
|
|
Start: startTime,
|
|
End: endTime,
|
|
Wght: stakeAmount,
|
|
},
|
|
Subnet: subnetID,
|
|
},
|
|
&signer.Empty{},
|
|
assetID,
|
|
owner,
|
|
&secp256k1fx.OutputOwners{},
|
|
reward.PercentDenominator,
|
|
)
|
|
defer cancel()
|
|
if err != nil {
|
|
return ids.Empty, err
|
|
}
|
|
return txID.ID(), err
|
|
}
|
|
|
|
func (d *LocalDeployer) StartServer() error {
|
|
isRunning, err := d.procChecker.IsServerProcessRunning(d.app)
|
|
if err != nil {
|
|
return fmt.Errorf("failed querying if server process is running: %w", err)
|
|
}
|
|
if !isRunning {
|
|
d.app.Log.Debug("gRPC server is not running")
|
|
if err := binutils.StartServerProcess(d.app); err != nil {
|
|
return fmt.Errorf("failed starting gRPC server process: %w", err)
|
|
}
|
|
d.backendStartedHere = true
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func GetCurrentSupply(subnetID ids.ID) error {
|
|
api := constants.LocalAPIEndpoint
|
|
pClient := platformvm.NewClient(api)
|
|
ctx, cancel := context.WithTimeout(context.Background(), constants.E2ERequestTimeout)
|
|
defer cancel()
|
|
_, _, err := pClient.GetCurrentSupply(ctx, subnetID)
|
|
return err
|
|
}
|
|
|
|
// BackendStartedHere returns true if the backend was started by this run,
|
|
// or false if it found it there already
|
|
func (d *LocalDeployer) BackendStartedHere() bool {
|
|
return d.backendStartedHere
|
|
}
|
|
|
|
// doDeploy the actual deployment to the network runner
|
|
// steps:
|
|
// - checks if the network has been started
|
|
// - install all needed plugin binaries, for the the new VM, and the already deployed VMs
|
|
// - either starts a network from the default snapshot if not started,
|
|
// or restarts the already available network while preserving state
|
|
// - waits completion of operation
|
|
// - get from the network an available subnet ID to be used in blockchain creation
|
|
// - deploy a new blockchain for the given VM ID, genesis, and available subnet ID
|
|
// - waits completion of operation
|
|
// - show status
|
|
func (d *LocalDeployer) doDeploy(chain string, chainGenesis []byte, genesisPath string) (ids.ID, ids.ID, error) {
|
|
nodeBinPath, err := d.SetupLocalEnv()
|
|
if err != nil {
|
|
return ids.Empty, ids.Empty, err
|
|
}
|
|
|
|
backendLogFile, err := binutils.GetBackendLogFile(d.app)
|
|
var backendLogDir string
|
|
if err == nil {
|
|
// TODO should we do something if there _was_ an error?
|
|
backendLogDir = filepath.Dir(backendLogFile)
|
|
}
|
|
|
|
cli, err := d.getClientFunc()
|
|
if err != nil {
|
|
return ids.Empty, ids.Empty, fmt.Errorf("error creating gRPC Client: %w", err)
|
|
}
|
|
defer cli.Close()
|
|
|
|
runDir := d.app.GetRunDir()
|
|
|
|
ctx := binutils.GetAsyncContext()
|
|
|
|
// loading sidecar before it's needed so we catch any error early
|
|
sc, err := d.app.LoadSidecar(chain)
|
|
if err != nil {
|
|
return ids.Empty, ids.Empty, fmt.Errorf("failed to load sidecar: %w", err)
|
|
}
|
|
|
|
// check for network status
|
|
networkBooted := true
|
|
clusterInfo, err := WaitForHealthy(ctx, cli)
|
|
rootDir := clusterInfo.GetRootDataDir()
|
|
if err != nil {
|
|
if !server.IsServerError(err, server.ErrNotBootstrapped) {
|
|
utils.FindErrorLogs(rootDir, backendLogDir)
|
|
return ids.Empty, ids.Empty, fmt.Errorf("failed to query network health: %w", err)
|
|
} else {
|
|
networkBooted = false
|
|
}
|
|
}
|
|
|
|
chainVMID, err := anrutils.VMID(chain)
|
|
if err != nil {
|
|
return ids.Empty, ids.Empty, fmt.Errorf("failed to create VM ID from %s: %w", chain, err)
|
|
}
|
|
d.app.Log.Debug("this VM will get ID", zap.String("vm-id", chainVMID.String()))
|
|
|
|
if !networkBooted {
|
|
if err := d.startNetwork(ctx, cli, nodeBinPath, runDir); err != nil {
|
|
utils.FindErrorLogs(rootDir, backendLogDir)
|
|
return ids.Empty, ids.Empty, err
|
|
}
|
|
}
|
|
|
|
// get VM info
|
|
clusterInfo, err = WaitForHealthy(ctx, cli)
|
|
if err != nil {
|
|
utils.FindErrorLogs(clusterInfo.GetRootDataDir(), backendLogDir)
|
|
return ids.Empty, ids.Empty, fmt.Errorf("failed to query network health: %w", err)
|
|
}
|
|
rootDir = clusterInfo.GetRootDataDir()
|
|
|
|
if alreadyDeployed(chainVMID, clusterInfo) {
|
|
ux.Logger.PrintToUser("Subnet %s has already been deployed", chain)
|
|
return ids.Empty, ids.Empty, nil
|
|
}
|
|
|
|
numBlockchains := len(clusterInfo.CustomChains)
|
|
|
|
subnetIDs := maps.Keys(clusterInfo.Subnets)
|
|
|
|
// in order to make subnet deploy faster, a set of validated subnet IDs is preloaded
|
|
// in the bootstrap snapshot
|
|
// we select one to be used for creating the next blockchain, for that we use the
|
|
// number of currently created blockchains as the index to select the next subnet ID,
|
|
// so we get incremental selection
|
|
sort.Strings(subnetIDs)
|
|
if len(subnetIDs) == 0 {
|
|
return ids.Empty, ids.Empty, errors.New("the network has not preloaded subnet IDs")
|
|
}
|
|
subnetIDStr := subnetIDs[numBlockchains%len(subnetIDs)]
|
|
|
|
// if a chainConfig has been configured
|
|
var (
|
|
chainConfig string
|
|
chainConfigFile = filepath.Join(d.app.GetSubnetDir(), chain, constants.ChainConfigFileName)
|
|
perNodeChainConfig string
|
|
perNodeChainConfigFile = filepath.Join(d.app.GetSubnetDir(), chain, constants.PerNodeChainConfigFileName)
|
|
)
|
|
if _, err := os.Stat(chainConfigFile); err == nil {
|
|
// currently the ANR only accepts the file as a path, not its content
|
|
chainConfig = chainConfigFile
|
|
}
|
|
if _, err := os.Stat(perNodeChainConfigFile); err == nil {
|
|
perNodeChainConfig = perNodeChainConfigFile
|
|
}
|
|
|
|
// install the plugin binary for the new VM
|
|
if err := d.installPlugin(chainVMID, d.vmBin); err != nil {
|
|
return ids.Empty, ids.Empty, err
|
|
}
|
|
|
|
ux.Logger.PrintToUser("VMs ready.")
|
|
|
|
// create a new blockchain on the already started network, associated to
|
|
// the given VM ID, genesis, and available subnet ID
|
|
blockchainSpecs := []*rpcpb.BlockchainSpec{
|
|
{
|
|
VmName: chain,
|
|
Genesis: genesisPath,
|
|
SubnetId: &subnetIDStr,
|
|
ChainConfig: chainConfig,
|
|
BlockchainAlias: chain,
|
|
PerNodeChainConfig: perNodeChainConfig,
|
|
},
|
|
}
|
|
deployBlockchainsInfo, err := cli.CreateBlockchains(
|
|
ctx,
|
|
blockchainSpecs,
|
|
)
|
|
if err != nil {
|
|
utils.FindErrorLogs(rootDir, backendLogDir)
|
|
pluginRemoveErr := d.removeInstalledPlugin(chainVMID)
|
|
if pluginRemoveErr != nil {
|
|
ux.Logger.PrintToUser("Failed to remove plugin binary: %s", pluginRemoveErr)
|
|
}
|
|
return ids.Empty, ids.Empty, fmt.Errorf("failed to deploy blockchain: %w", err)
|
|
}
|
|
rootDir = clusterInfo.GetRootDataDir()
|
|
|
|
d.app.Log.Debug(deployBlockchainsInfo.String())
|
|
|
|
fmt.Println()
|
|
ux.Logger.PrintToUser("Blockchain has been deployed. Wait until network acknowledges...")
|
|
|
|
clusterInfo, err = WaitForHealthy(ctx, cli)
|
|
if err != nil {
|
|
utils.FindErrorLogs(rootDir, backendLogDir)
|
|
pluginRemoveErr := d.removeInstalledPlugin(chainVMID)
|
|
if pluginRemoveErr != nil {
|
|
ux.Logger.PrintToUser("Failed to remove plugin binary: %s", pluginRemoveErr)
|
|
}
|
|
return ids.Empty, ids.Empty, fmt.Errorf("failed to query network health: %w", err)
|
|
}
|
|
|
|
endpoint := GetFirstEndpoint(clusterInfo, chain)
|
|
|
|
fmt.Println()
|
|
ux.Logger.PrintToUser("Network ready to use. Local network node endpoints:")
|
|
ux.PrintTableEndpoints(clusterInfo)
|
|
fmt.Println()
|
|
|
|
ux.Logger.PrintToUser("Browser Extension connection details (any node URL from above works):")
|
|
ux.Logger.PrintToUser("RPC URL: %s", endpoint[strings.LastIndex(endpoint, "http"):])
|
|
|
|
if sc.VM == models.EVM {
|
|
if err := d.printExtraEvmInfo(chain, chainGenesis); err != nil {
|
|
// not supposed to happen due to genesis pre validation
|
|
return ids.Empty, ids.Empty, nil
|
|
}
|
|
}
|
|
|
|
// we can safely ignore errors here as the subnets have already been generated
|
|
subnetID, _ := ids.FromString(subnetIDStr)
|
|
var blockchainID ids.ID
|
|
for _, info := range clusterInfo.CustomChains {
|
|
if info.VmId == chainVMID.String() {
|
|
blockchainID, _ = ids.FromString(info.ChainId)
|
|
}
|
|
}
|
|
return subnetID, blockchainID, nil
|
|
}
|
|
|
|
func (d *LocalDeployer) printExtraEvmInfo(chain string, chainGenesis []byte) error {
|
|
var evmGenesis core.Genesis
|
|
if err := json.Unmarshal(chainGenesis, &evmGenesis); err != nil {
|
|
return fmt.Errorf("failed to unmarshall genesis: %w", err)
|
|
}
|
|
for address := range evmGenesis.Alloc {
|
|
amount := evmGenesis.Alloc[address].Balance
|
|
formattedAmount := new(big.Int).Div(amount, big.NewInt(params.Ether))
|
|
if address == vm.PrefundedEwoqAddress {
|
|
ux.Logger.PrintToUser("Funded address: %s with %s (10^18) - private key: %s", address, formattedAmount.String(), vm.PrefundedEwoqPrivate)
|
|
} else {
|
|
ux.Logger.PrintToUser("Funded address: %s with %s", address, formattedAmount.String())
|
|
}
|
|
}
|
|
ux.Logger.PrintToUser("Network name: %s", chain)
|
|
ux.Logger.PrintToUser("Chain ID: %s", evmGenesis.Config.ChainID)
|
|
ux.Logger.PrintToUser("Currency Symbol: %s", d.app.GetTokenName(chain))
|
|
return nil
|
|
}
|
|
|
|
// SetupLocalEnv also does some heavy lifting:
|
|
// * sets up default snapshot if not installed
|
|
// * checks if node is installed in the local binary path
|
|
// * if not, it downloads it and installs it (os - and archive dependent)
|
|
// * returns the location of the node path
|
|
func (d *LocalDeployer) SetupLocalEnv() (string, error) {
|
|
err := d.setDefaultSnapshot(d.app.GetSnapshotsDir(), false)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed setting up snapshots: %w", err)
|
|
}
|
|
|
|
luxDir, err := d.setupLocalEnv()
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed setting up local environment: %w", err)
|
|
}
|
|
|
|
pluginDir := d.app.GetPluginsDir()
|
|
nodeBinPath := filepath.Join(luxDir, "node")
|
|
|
|
if err := os.MkdirAll(pluginDir, constants.DefaultPerms755); err != nil {
|
|
return "", fmt.Errorf("could not create pluginDir %s", pluginDir)
|
|
}
|
|
|
|
exists, err := storage.FolderExists(pluginDir)
|
|
if !exists || err != nil {
|
|
return "", fmt.Errorf("evaluated pluginDir to be %s but it does not exist", pluginDir)
|
|
}
|
|
|
|
// TODO: we need some better version management here
|
|
// * compare latest to local version
|
|
// * decide if force update or give user choice
|
|
exists, err = storage.FileExists(nodeBinPath)
|
|
if !exists || err != nil {
|
|
return "", fmt.Errorf(
|
|
"evaluated nodeBinPath to be %s but it does not exist", nodeBinPath)
|
|
}
|
|
|
|
return nodeBinPath, nil
|
|
}
|
|
|
|
func (d *LocalDeployer) setupLocalEnv() (string, error) {
|
|
_, binPath, err := binutils.SetupLuxgo(d.app, d.luxVersion)
|
|
return binPath, err
|
|
}
|
|
|
|
// WaitForHealthy polls continuously until the network is ready to be used
|
|
func WaitForHealthy(
|
|
ctx context.Context,
|
|
cli client.Client,
|
|
) (*rpcpb.ClusterInfo, error) {
|
|
cancel := make(chan struct{})
|
|
defer close(cancel)
|
|
go ux.PrintWait(cancel)
|
|
resp, err := cli.WaitForHealthy(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return resp.ClusterInfo, nil
|
|
}
|
|
|
|
// GetFirstEndpoint get a human readable endpoint for the given chain
|
|
func GetFirstEndpoint(clusterInfo *rpcpb.ClusterInfo, chain string) string {
|
|
var endpoint string
|
|
for _, nodeInfo := range clusterInfo.NodeInfos {
|
|
for blockchainID, chainInfo := range clusterInfo.CustomChains {
|
|
if chainInfo.ChainName == chain && nodeInfo.Name == clusterInfo.NodeNames[0] {
|
|
endpoint = fmt.Sprintf("Endpoint at node %s for blockchain %q with VM ID %q: %s/ext/bc/%s/rpc", nodeInfo.Name, blockchainID, chainInfo.VmId, nodeInfo.GetUri(), blockchainID)
|
|
}
|
|
}
|
|
}
|
|
return endpoint
|
|
}
|
|
|
|
// HasEndpoints returns true if cluster info contains custom blockchains
|
|
func HasEndpoints(clusterInfo *rpcpb.ClusterInfo) bool {
|
|
return len(clusterInfo.CustomChains) > 0
|
|
}
|
|
|
|
// return true if vm has already been deployed
|
|
func alreadyDeployed(chainVMID ids.ID, clusterInfo *rpcpb.ClusterInfo) bool {
|
|
if clusterInfo != nil {
|
|
for _, chainInfo := range clusterInfo.CustomChains {
|
|
if chainInfo.VmId == chainVMID.String() {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// get list of all needed plugins and install them
|
|
func (d *LocalDeployer) installPlugin(
|
|
vmID ids.ID,
|
|
vmBin string,
|
|
) error {
|
|
return d.binaryDownloader.InstallVM(vmID.String(), vmBin)
|
|
}
|
|
|
|
// get list of all needed plugins and install them
|
|
func (d *LocalDeployer) removeInstalledPlugin(
|
|
vmID ids.ID,
|
|
) error {
|
|
return d.binaryDownloader.RemoveVM(vmID.String())
|
|
}
|
|
|
|
func getExpectedDefaultSnapshotSHA256Sum() (string, error) {
|
|
resp, err := http.Get(constants.BootstrapSnapshotSHA256URL)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed downloading sha256 sums: %w", err)
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
return "", fmt.Errorf("failed downloading sha256 sums: unexpected http status code: %d", resp.StatusCode)
|
|
}
|
|
defer resp.Body.Close()
|
|
sha256FileBytes, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed downloading sha256 sums: %w", err)
|
|
}
|
|
expectedSum, err := utils.SearchSHA256File(sha256FileBytes, constants.BootstrapSnapshotLocalPath)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed obtaining snapshot sha256 sum: %w", err)
|
|
}
|
|
return expectedSum, nil
|
|
}
|
|
|
|
// Initialize default snapshot with bootstrap snapshot archive
|
|
// If force flag is set to true, overwrite the default snapshot if it exists
|
|
func SetDefaultSnapshot(snapshotsDir string, force bool) error {
|
|
bootstrapSnapshotArchivePath := filepath.Join(snapshotsDir, constants.BootstrapSnapshotArchiveName)
|
|
// will download either if file not exists or if sha256 sum is not the same
|
|
downloadSnapshot := false
|
|
if _, err := os.Stat(bootstrapSnapshotArchivePath); os.IsNotExist(err) {
|
|
downloadSnapshot = true
|
|
} else {
|
|
gotSum, err := utils.GetSHA256FromDisk(bootstrapSnapshotArchivePath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
expectedSum, err := getExpectedDefaultSnapshotSHA256Sum()
|
|
if err != nil {
|
|
ux.Logger.PrintToUser("Warning: failure verifying that the local snapshot is the latest one: %s", err)
|
|
} else if gotSum != expectedSum {
|
|
downloadSnapshot = true
|
|
}
|
|
}
|
|
if downloadSnapshot {
|
|
resp, err := http.Get(constants.BootstrapSnapshotURL)
|
|
if err != nil {
|
|
return fmt.Errorf("failed downloading bootstrap snapshot: %w", err)
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
return fmt.Errorf("failed downloading bootstrap snapshot: unexpected http status code: %d", resp.StatusCode)
|
|
}
|
|
defer resp.Body.Close()
|
|
bootstrapSnapshotBytes, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return fmt.Errorf("failed downloading bootstrap snapshot: %w", err)
|
|
}
|
|
if err := os.WriteFile(bootstrapSnapshotArchivePath, bootstrapSnapshotBytes, WriteReadReadPerms); err != nil {
|
|
return fmt.Errorf("failed writing down bootstrap snapshot: %w", err)
|
|
}
|
|
}
|
|
defaultSnapshotPath := filepath.Join(snapshotsDir, "anr-snapshot-"+constants.DefaultSnapshotName)
|
|
if force {
|
|
if err := os.RemoveAll(defaultSnapshotPath); err != nil {
|
|
return fmt.Errorf("failed removing default snapshot: %w", err)
|
|
}
|
|
}
|
|
if _, err := os.Stat(defaultSnapshotPath); os.IsNotExist(err) {
|
|
bootstrapSnapshotBytes, err := os.ReadFile(bootstrapSnapshotArchivePath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed reading bootstrap snapshot: %w", err)
|
|
}
|
|
if err := binutils.InstallArchive("tar.gz", bootstrapSnapshotBytes, snapshotsDir); err != nil {
|
|
return fmt.Errorf("failed installing bootstrap snapshot: %w", err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// start the network
|
|
func (d *LocalDeployer) startNetwork(
|
|
ctx context.Context,
|
|
cli client.Client,
|
|
nodeBinPath string,
|
|
runDir string,
|
|
) error {
|
|
loadSnapshotOpts := []client.OpOption{
|
|
client.WithExecPath(nodeBinPath),
|
|
client.WithRootDataDir(runDir),
|
|
client.WithReassignPortsIfUsed(true),
|
|
client.WithPluginDir(d.app.GetPluginsDir()),
|
|
}
|
|
|
|
// load global node configs if they exist
|
|
configStr, err := d.app.Conf.LoadNodeConfig()
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
if configStr != "" {
|
|
loadSnapshotOpts = append(loadSnapshotOpts, client.WithGlobalNodeConfig(configStr))
|
|
}
|
|
|
|
pp, err := cli.LoadSnapshot(
|
|
ctx,
|
|
constants.DefaultSnapshotName,
|
|
loadSnapshotOpts...,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to start network :%w", err)
|
|
}
|
|
ux.Logger.PrintToUser("Node log path: %s/node<i>/logs", pp.ClusterInfo.RootDataDir)
|
|
ux.Logger.PrintToUser("Starting network...")
|
|
return nil
|
|
}
|
|
|
|
// Returns an error if the server cannot be contacted. You may want to ignore this error.
|
|
func GetLocallyDeployedSubnets() (map[string]struct{}, error) {
|
|
deployedNames := map[string]struct{}{}
|
|
// if the server can not be contacted, or there is a problem with the query,
|
|
// DO NOT FAIL, just print No for deployed status
|
|
cli, err := binutils.NewGRPCClient()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ctx := binutils.GetAsyncContext()
|
|
resp, err := cli.Status(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, chain := range resp.GetClusterInfo().CustomChains {
|
|
deployedNames[chain.ChainName] = struct{}{}
|
|
}
|
|
|
|
return deployedNames, nil
|
|
}
|
|
|
|
func IssueRemoveSubnetValidatorTx(kc keychain.Keychain, subnetID ids.ID, nodeID ids.NodeID) (ids.ID, error) {
|
|
ctx := context.Background()
|
|
api := constants.LocalAPIEndpoint
|
|
// Create empty EthKeychain if kc doesn't implement it
|
|
var ethKc c.EthKeychain
|
|
if ekc, ok := kc.(c.EthKeychain); ok {
|
|
ethKc = ekc
|
|
} else {
|
|
// Create a minimal EthKeychain implementation
|
|
ethKc = &emptyEthKeychain{}
|
|
}
|
|
wallet, err := primary.MakeWallet(ctx, api, kc, ethKc, primary.WalletConfig{})
|
|
if err != nil {
|
|
return ids.Empty, err
|
|
}
|
|
|
|
tx, err := wallet.P().IssueRemoveSubnetValidatorTx(nodeID, subnetID)
|
|
if err != nil {
|
|
return ids.Empty, err
|
|
}
|
|
return tx.ID(), nil
|
|
}
|
|
|
|
func GetSubnetValidators(subnetID ids.ID) ([]platformvm.ClientPermissionlessValidator, error) {
|
|
api := constants.LocalAPIEndpoint
|
|
pClient := platformvm.NewClient(api)
|
|
ctx, cancel := context.WithTimeout(context.Background(), constants.E2ERequestTimeout)
|
|
defer cancel()
|
|
|
|
return pClient.GetCurrentValidators(ctx, subnetID, nil)
|
|
}
|
|
|
|
func CheckNodeIsInSubnetPendingValidators(subnetID ids.ID, nodeID string) (bool, error) {
|
|
api := constants.LocalAPIEndpoint
|
|
pClient := platformvm.NewClient(api)
|
|
ctx, cancel := context.WithTimeout(context.Background(), constants.E2ERequestTimeout)
|
|
defer cancel()
|
|
|
|
// Get validators that will be active in the future (pending validators)
|
|
futureTime := uint64(time.Now().Add(time.Hour).Unix())
|
|
validators, err := pClient.GetValidatorsAt(ctx, subnetID, platformvmapi.Height(futureTime))
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
// Convert nodeID string to ids.NodeID for comparison
|
|
nID, err := ids.NodeIDFromString(nodeID)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
// Check current validators
|
|
currentValidators, err := pClient.GetCurrentValidators(ctx, subnetID, nil)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
// Check if the node is in future validators but not in current validators
|
|
inFuture := false
|
|
for id := range validators {
|
|
if id == nID {
|
|
inFuture = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !inFuture {
|
|
return false, nil
|
|
}
|
|
|
|
// Check if it's already a current validator
|
|
for _, v := range currentValidators {
|
|
if v.NodeID == nID {
|
|
return false, nil // Already active, not pending
|
|
}
|
|
}
|
|
|
|
return true, nil // In future but not current = pending
|
|
}
|