Files

367 lines
11 KiB
Go

// Copyright (C) 2022, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package subnetcmd
import (
"context"
"errors"
"fmt"
"os"
"time"
"github.com/luxfi/cli/v2/pkg/constants"
"github.com/luxfi/cli/v2/pkg/models"
"github.com/luxfi/cli/v2/pkg/prompts"
"github.com/luxfi/cli/v2/pkg/subnet"
"github.com/luxfi/cli/v2/pkg/txutils"
"github.com/luxfi/cli/v2/pkg/ux"
"github.com/luxfi/ids"
lux_constants "github.com/luxfi/node/v2/v2/utils/constants"
"github.com/luxfi/node/v2/v2/vms/platformvm"
"github.com/spf13/cobra"
)
var (
nodeIDStr string
weight uint64
startTimeStr string
duration time.Duration
errNoSubnetID = errors.New("failed to find the subnet ID for this subnet, has it been deployed/created on this network?")
)
// lux subnet deploy
func newAddValidatorCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "addValidator [subnetName]",
Short: "Allow a validator to validate your subnet",
Long: `The subnet addValidator command whitelists a primary network validator to
validate the provided deployed Subnet.
To add the validator to the Subnet's allow list, you first need to provide
the subnetName and the validator's unique NodeID. The command then prompts
for the validation start time, duration, and stake weight. You can bypass
these prompts by providing the values with flags.
This command currently only works on Subnets deployed to either the Testnet
Testnet or Mainnet.`,
SilenceUsage: true,
RunE: addValidator,
Args: cobra.ExactArgs(1),
}
cmd.Flags().StringVarP(&keyName, "key", "k", "", "select the key to use [testnet deploy only]")
cmd.Flags().StringVar(&nodeIDStr, "nodeID", "", "set the NodeID of the validator to add")
cmd.Flags().Uint64Var(&weight, "weight", 0, "set the staking weight of the validator to add")
cmd.Flags().StringVar(&startTimeStr, "start-time", "", "UTC start time when this validator starts validating, in 'YYYY-MM-DD HH:MM:SS' format")
cmd.Flags().DurationVar(&duration, "staking-period", 0, "how long this validator will be staking")
cmd.Flags().BoolVar(&deployTestnet, "testnet", false, "join on `testnet` (alias for `testnet`)")
cmd.Flags().BoolVar(&deployMainnet, "mainnet", false, "join on `mainnet`")
cmd.Flags().StringSliceVar(&subnetAuthKeys, "subnet-auth-keys", nil, "control keys that will be used to authenticate add validator tx")
cmd.Flags().StringVar(&outputTxPath, "output-tx-path", "", "file path of the add validator tx")
cmd.Flags().BoolVarP(&useLedger, "ledger", "g", false, "use ledger instead of key (always true on mainnet, defaults to false on testnet)")
cmd.Flags().StringSliceVar(&ledgerAddresses, "ledger-addrs", []string{}, "use the given ledger addresses")
return cmd
}
func addValidator(_ *cobra.Command, args []string) error {
var (
nodeID ids.NodeID
start time.Time
err error
)
var network models.Network
switch {
case deployTestnet:
network = models.Testnet
case deployMainnet:
network = models.Mainnet
}
if network == models.Undefined {
networkStr, err := app.Prompt.CaptureList(
"Choose a network to add validator to.",
[]string{models.Testnet.String(), models.Mainnet.String()},
)
if err != nil {
return err
}
network = models.NetworkFromString(networkStr)
}
if outputTxPath != "" {
if _, err := os.Stat(outputTxPath); err == nil {
return fmt.Errorf("outputTxPath %q already exists", outputTxPath)
}
}
if len(ledgerAddresses) > 0 {
useLedger = true
}
if useLedger && keyName != "" {
return ErrMutuallyExlusiveKeyLedger
}
switch network {
case models.Testnet:
if !useLedger && keyName == "" {
useLedger, keyName, err = prompts.GetTestnetKeyOrLedger(app.Prompt, "pay transaction fees", app.GetKeyDir())
if err != nil {
return err
}
}
case models.Mainnet:
useLedger = true
if keyName != "" {
return ErrStoredKeyOnMainnet
}
default:
return errors.New("unsupported network")
}
// used in E2E to simulate public network execution paths on a local network
if os.Getenv(constants.SimulatePublicNetwork) != "" {
network = models.Local
}
chains, err := validateSubnetNameAndGetChains(args)
if err != nil {
return err
}
subnetName := chains[0]
sc, err := app.LoadSidecar(subnetName)
if err != nil {
return err
}
subnetID := sc.Networks[network.String()].SubnetID
if subnetID == ids.Empty {
return errNoSubnetID
}
controlKeys, threshold, err := txutils.GetOwners(network, subnetID)
if err != nil {
return err
}
// get keys for add validator tx signing
if subnetAuthKeys != nil {
if err := prompts.CheckSubnetAuthKeys(subnetAuthKeys, controlKeys, threshold); err != nil {
return err
}
} else {
subnetAuthKeys, err = prompts.GetSubnetAuthKeys(app.Prompt, controlKeys, threshold)
if err != nil {
return err
}
}
ux.Logger.PrintToUser("Your subnet auth keys for add validator tx creation: %s", subnetAuthKeys)
if nodeIDStr == "" {
nodeID, err = promptNodeID()
if err != nil {
return err
}
} else {
nodeID, err = ids.NodeIDFromString(nodeIDStr)
if err != nil {
return err
}
}
if weight == 0 {
weight, err = promptWeight()
if err != nil {
return err
}
} else if weight < constants.MinStakeWeight {
return fmt.Errorf("illegal weight, must be greater than or equal to %d: %d", constants.MinStakeWeight, weight)
}
start, duration, err = getTimeParameters(network, nodeID)
if err != nil {
return err
}
ux.Logger.PrintToUser("NodeID: %s", nodeID.String())
ux.Logger.PrintToUser("Network: %s", network.String())
ux.Logger.PrintToUser("Start time: %s", start.Format(constants.TimeParseLayout))
ux.Logger.PrintToUser("End time: %s", start.Add(duration).Format(constants.TimeParseLayout))
ux.Logger.PrintToUser("Weight: %d", weight)
ux.Logger.PrintToUser("Inputs complete, issuing transaction to add the provided validator information...")
// get keychain accesor
kc, err := GetKeychain(useLedger, ledgerAddresses, keyName, network)
if err != nil {
return err
}
deployer := subnet.NewPublicDeployer(app, useLedger, kc, network)
isFullySigned, tx, remainingSubnetAuthKeys, err := deployer.AddValidator(controlKeys, subnetAuthKeys, subnetID, nodeID, weight, start, duration)
if err != nil {
return err
}
if !isFullySigned {
if err := SaveNotFullySignedTx(
"Add Validator",
tx,
subnetName,
subnetAuthKeys,
remainingSubnetAuthKeys,
outputTxPath,
false,
); err != nil {
return err
}
}
return err
}
func promptDuration(start time.Time) (time.Duration, error) {
for {
txt := "How long should this validator be validating? Enter a duration, e.g. 8760h. Valid time units are \"ns\", \"us\" (or \"µs\"), \"ms\", \"s\", \"m\", \"h\""
d, err := app.Prompt.CaptureDuration(txt)
if err != nil {
return 0, err
}
end := start.Add(d)
confirm := fmt.Sprintf("Your validator will finish staking by %s", end.Format(constants.TimeParseLayout))
yes, err := app.Prompt.CaptureYesNo(confirm)
if err != nil {
return 0, err
}
if yes {
return d, nil
}
}
}
func getMaxValidationTime(network models.Network, nodeID ids.NodeID, startTime time.Time) (time.Duration, error) {
var uri string
switch network {
case models.Testnet:
uri = constants.TestnetAPIEndpoint
case models.Mainnet:
uri = constants.MainnetAPIEndpoint
case models.Local:
// used for E2E testing of public related paths
uri = constants.LocalAPIEndpoint
default:
return 0, fmt.Errorf("unsupported public network")
}
ctx := context.Background()
ctx, cancel := context.WithTimeout(ctx, constants.RequestTimeout)
platformCli := platformvm.NewClient(uri)
vs, err := platformCli.GetCurrentValidators(ctx, lux_constants.PrimaryNetworkID, nil)
cancel()
if err != nil {
return 0, err
}
for _, v := range vs {
if v.NodeID == nodeID {
return time.Unix(int64(v.EndTime), 0).Sub(startTime), nil
}
}
return 0, errors.New("nodeID not found in validator set: " + nodeID.String())
}
func getTimeParameters(network models.Network, nodeID ids.NodeID) (time.Time, time.Duration, error) {
var (
start time.Time
err error
)
const (
defaultStartOption = "Start in one minute"
defaultDurationOption = "Until primary network validator expires"
custom = "Custom"
)
if startTimeStr == "" {
ux.Logger.PrintToUser("When should your validator start validating?\n" +
"If you validator is not ready by this time, subnet downtime can occur.")
startTimeOptions := []string{defaultStartOption, custom}
startTimeOption, err := app.Prompt.CaptureList("Start time", startTimeOptions)
if err != nil {
return time.Time{}, 0, err
}
switch startTimeOption {
case defaultStartOption:
start = time.Now().Add(constants.StakingStartLeadTime)
default:
start, err = promptStart()
if err != nil {
return time.Time{}, 0, err
}
}
} else {
start, err = time.Parse(constants.TimeParseLayout, startTimeStr)
if err != nil {
return time.Time{}, 0, err
}
if start.Before(time.Now().Add(constants.StakingMinimumLeadTime)) {
return time.Time{}, 0, fmt.Errorf("time should be at least %s in the future ", constants.StakingMinimumLeadTime)
}
}
if duration == 0 {
msg := "How long should your validator validate for?"
durationOptions := []string{defaultDurationOption, custom}
durationOption, err := app.Prompt.CaptureList(msg, durationOptions)
if err != nil {
return time.Time{}, 0, err
}
switch durationOption {
case defaultDurationOption:
duration, err = getMaxValidationTime(network, nodeID, start)
if err != nil {
return time.Time{}, 0, err
}
default:
duration, err = promptDuration(start)
if err != nil {
return time.Time{}, 0, err
}
}
}
return start, duration, nil
}
func promptStart() (time.Time, error) {
txt := "When should the validator start validating? Enter a UTC datetime in 'YYYY-MM-DD HH:MM:SS' format"
return app.Prompt.CaptureDate(txt)
}
func promptNodeID() (ids.NodeID, error) {
ux.Logger.PrintToUser("Next, we need the NodeID of the validator you want to whitelist.")
ux.Logger.PrintToUser("")
ux.Logger.PrintToUser("Check https://docs.lux.network/apis/node/apis/info#infogetnodeid for instructions about how to query the NodeID from your node")
ux.Logger.PrintToUser("(Edit host IP address and port to match your deployment, if needed).")
txt := "What is the NodeID of the validator you'd like to whitelist?"
return app.Prompt.CaptureNodeID(txt)
}
func promptWeight() (uint64, error) {
defaultWeight := fmt.Sprintf("Default (%d)", constants.DefaultStakeWeight)
txt := "What stake weight would you like to assign to the validator?"
weightOptions := []string{defaultWeight, "Custom"}
weightOption, err := app.Prompt.CaptureList(txt, weightOptions)
if err != nil {
return 0, err
}
switch weightOption {
case defaultWeight:
return constants.DefaultStakeWeight, nil
default:
return app.Prompt.CaptureWeight(txt)
}
}