Files

941 lines
31 KiB
Go

// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package blockchaincmd
import (
"errors"
"fmt"
"strings"
"time"
sdkutils "github.com/luxfi/cli/v2/sdk/utils"
"github.com/spf13/pflag"
"github.com/luxfi/cli/v2/cmd/flags"
"github.com/luxfi/cli/v2/pkg/blockchain"
"github.com/luxfi/cli/v2/pkg/cobrautils"
"github.com/luxfi/cli/v2/pkg/constants"
"github.com/luxfi/cli/v2/pkg/contract"
"github.com/luxfi/cli/v2/pkg/keychain"
"github.com/luxfi/cli/v2/pkg/localnet"
"github.com/luxfi/cli/v2/pkg/models"
"github.com/luxfi/cli/v2/pkg/networkoptions"
"github.com/luxfi/cli/v2/pkg/prompts"
"github.com/luxfi/cli/v2/pkg/signatureaggregator"
"github.com/luxfi/cli/v2/pkg/subnet"
"github.com/luxfi/cli/v2/pkg/txutils"
"github.com/luxfi/cli/v2/pkg/utils"
"github.com/luxfi/cli/v2/pkg/ux"
"github.com/luxfi/cli/v2/pkg/validatormanager"
"github.com/luxfi/cli/v2/sdk/evm"
"github.com/luxfi/cli/v2/sdk/validator"
"github.com/luxfi/ids"
luxdconstants "github.com/luxfi/node/v2/v2/utils/constants"
"github.com/luxfi/node/v2/v2/utils/formatting/address"
"github.com/luxfi/node/v2/v2/utils/logging"
"github.com/luxfi/node/v2/v2/utils/units"
warpMessage "github.com/luxfi/warp"
"github.com/ethereum/go-ethereum/common"
"github.com/spf13/cobra"
)
var (
nodeIDStr string
nodeEndpoint string
balanceLUX float64
weight uint64
startTimeStr string
duration time.Duration
defaultValidatorParams bool
useDefaultStartTime bool
useDefaultDuration bool
useDefaultWeight bool
waitForTxAcceptance bool
publicKey string
pop string
remainingBalanceOwnerAddr string
disableOwnerAddr string
rewardsRecipientAddr string
delegationFee uint16
errNoSubnetID = errors.New("failed to find the subnet ID for this subnet, has it been deployed/created on this network?")
errMutuallyExclusiveDurationOptions = errors.New("--use-default-duration/--use-default-validator-params and --staking-period are mutually exclusive")
errMutuallyExclusiveStartOptions = errors.New("--use-default-start-time/--use-default-validator-params and --start-time are mutually exclusive")
errMutuallyExclusiveWeightOptions = errors.New("--use-default-validator-params and --weight are mutually exclusive")
ErrNotPermissionedSubnet = errors.New("subnet is not permissioned")
clusterNameFlagValue string
createLocalValidator bool
externalValidatorManagerOwner bool
validatorManagerOwner string
httpPort uint32
stakingPort uint32
addValidatorFlags BlockchainAddValidatorFlags
)
type BlockchainAddValidatorFlags struct {
RPC string
SigAggFlags flags.SignatureAggregatorFlags
}
const (
validatorWeightFlag = "weight"
)
// lux blockchain addValidator
func newAddValidatorCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "addValidator [blockchainName]",
Short: "Add a validator to an L1",
Long: `The blockchain addValidator command adds a node as a validator to
an L1 of the user provided deployed network. If the network is proof of
authority, the owner of the validator manager contract must sign the
transaction. If the network is proof of stake, the node must stake the L1's
staking token. Both processes will issue a RegisterL1ValidatorTx on the P-Chain.
This command currently only works on Blockchains deployed to either the Testnet
Testnet or Mainnet.`,
RunE: addValidator,
PreRunE: cobrautils.MaximumNArgs(1),
}
networkGroup := networkoptions.GetNetworkFlagsGroup(cmd, &globalNetworkFlags, true, networkoptions.DefaultSupportedNetworkOptions)
flags.AddRPCFlagToCmd(cmd, app, &addValidatorFlags.RPC)
sigAggGroup := flags.AddSignatureAggregatorFlagsToCmd(cmd, &addValidatorFlags.SigAggFlags)
cmd.Flags().StringVarP(&keyName, "key", "k", "", "select the key to use [testnet/devnet only]")
cmd.Flags().Float64Var(
&balanceLUX,
"balance",
0,
"set the LUX balance of the validator that will be used for continuous fee on P-Chain",
)
cmd.Flags().BoolVarP(&useEwoq, "ewoq", "e", false, "use ewoq key [testnet/devnet only]")
cmd.Flags().BoolVarP(&useLedger, "ledger", "g", false, "use ledger instead of key (always true on mainnet, defaults to false on testnet/devnet)")
cmd.Flags().StringSliceVar(&ledgerAddresses, "ledger-addrs", []string{}, "use the given ledger addresses")
cmd.Flags().StringVar(&nodeIDStr, "node-id", "", "node-id of the validator to add")
cmd.Flags().StringVar(&publicKey, "bls-public-key", "", "set the BLS public key of the validator to add")
cmd.Flags().StringVar(&pop, "bls-proof-of-possession", "", "set the BLS proof of possession of the validator to add")
cmd.Flags().StringVar(&remainingBalanceOwnerAddr, "remaining-balance-owner", "", "P-Chain address that will receive any leftover LUX from the validator when it is removed from Subnet")
cmd.Flags().StringVar(&disableOwnerAddr, "disable-owner", "", "P-Chain address that will able to disable the validator with a P-Chain transaction")
cmd.Flags().StringVar(&rewardsRecipientAddr, "rewards-recipient", "", "EVM address that will receive the validation rewards")
cmd.Flags().BoolVar(&createLocalValidator, "create-local-validator", false, "create additional local validator and add it to existing running local node")
cmd.Flags().BoolVar(&partialSync, "partial-sync", true, "set primary network partial sync for new validators")
cmd.Flags().StringVar(&nodeEndpoint, "node-endpoint", "", "gather node id/bls from publicly available luxd apis on the given endpoint")
cmd.Flags().Uint64Var(&weight, validatorWeightFlag, uint64(constants.DefaultStakeWeight), "set the weight of the validator")
cmd.Flags().StringVar(&validatorManagerOwner, "validator-manager-owner", "", "force using this address to issue transactions to the validator manager")
remoteBlockchainGroup := flags.RegisterFlagGroup(cmd, "Add Validator To Remote Blockchain Flags (Blockchain config is not in local machine)", "show-remote-blockchain-flags", true, func(set *pflag.FlagSet) {
set.StringVar(&subnetIDstr, "subnet-id", "", "subnet ID (only if blockchain name is not provided)")
})
nonSovGroup := flags.RegisterFlagGroup(cmd, "Non Subnet-Only-Validators (Non-SOV) Flags", "show-non-sov-flags", false, func(set *pflag.FlagSet) {
set.BoolVar(&useDefaultStartTime, "default-start-time", false, "(for Subnets, not L1s) use default start time for subnet validator (5 minutes later for testnet & mainnet, 30 seconds later for devnet)")
set.StringVar(&startTimeStr, "start-time", "", "(for Subnets, not L1s) UTC start time when this validator starts validating, in 'YYYY-MM-DD HH:MM:SS' format")
set.BoolVar(&useDefaultDuration, "default-duration", false, "(for Subnets, not L1s) set duration so as to validate until primary validator ends its period")
set.BoolVar(&defaultValidatorParams, "default-validator-params", false, "(for Subnets, not L1s) use default weight/start/duration params for subnet validator")
set.StringSliceVar(&subnetAuthKeys, "subnet-auth-keys", nil, "(for Subnets, not L1s) control keys that will be used to authenticate add validator tx")
set.StringVar(&outputTxPath, "output-tx-path", "", "(for Subnets, not L1s) file path of the add validator tx")
set.BoolVar(&waitForTxAcceptance, "wait-for-tx-acceptance", true, "(for Subnets, not L1s) just issue the add validator tx, without waiting for its acceptance")
set.DurationVar(&duration, "staking-period", 0, "how long this validator will be staking")
})
localMachineGroup := flags.RegisterFlagGroup(cmd, "Local Machine Flags (Use local machine as a validator)", "show-local-machine-flags", false, func(set *pflag.FlagSet) {
set.Uint32Var(&httpPort, "http-port", 0, "http port for node")
set.Uint32Var(&stakingPort, "staking-port", 0, "staking port for node")
set.BoolVar(&partialSync, "partial-sync", true, "set primary network partial sync for new validators")
set.BoolVar(&createLocalValidator, "create-local-validator", false, "create additional local validator and add it to existing running local node")
})
posGroup := flags.RegisterFlagGroup(cmd, "Proof Of Stake Flags", "show-pos-flags", false, func(set *pflag.FlagSet) {
set.Uint16Var(&delegationFee, "delegation-fee", 100, "(PoS only) delegation fee (in bips)")
set.DurationVar(&duration, "staking-period", 0, "how long this validator will be staking")
})
externalSigningGroup := flags.RegisterFlagGroup(cmd, "External EVM Signature Flags (For EVM Multisig and Ledger Signing)", "show-external-signing-flags", true, func(set *pflag.FlagSet) {
set.BoolVar(&externalValidatorManagerOwner, "external-evm-signature", false, "set this value to true when signing validator manager tx outside of cli (for multisig or ledger)")
set.StringVar(&initiateTxHash, "initiate-tx-hash", "", "initiate tx is already issued, with the given hash")
})
cmd.SetHelpFunc(flags.WithGroupedHelp([]flags.GroupedFlags{networkGroup, externalSigningGroup, remoteBlockchainGroup, localMachineGroup, posGroup, nonSovGroup, sigAggGroup}))
return cmd
}
func preAddChecks(args []string) error {
if nodeEndpoint != "" && createLocalValidator {
return fmt.Errorf("cannot set both --node-endpoint and --create-local-validator")
}
if createLocalValidator && (nodeIDStr != "" || publicKey != "" || pop != "") {
return fmt.Errorf("cannot set --node-id, --bls-public-key or --bls-proof-of-possession if --create-local-validator used")
}
if len(args) == 0 && createLocalValidator {
return fmt.Errorf("use lux addValidator <subnetName> command to use local machine as new validator")
}
return nil
}
func addValidator(cmd *cobra.Command, args []string) error {
var sc models.Sidecar
blockchainName := ""
networkOption := networkoptions.DefaultSupportedNetworkOptions
if len(args) == 1 {
blockchainName = args[0]
_, err := ValidateSubnetNameAndGetChains([]string{blockchainName})
if err != nil {
return err
}
sc, err = app.LoadSidecar(blockchainName)
if err != nil {
return fmt.Errorf("failed to load sidecar: %w", err)
}
networkOption = networkoptions.GetNetworkFromSidecar(sc, networkoptions.DefaultSupportedNetworkOptions)
}
network, err := networkoptions.GetNetworkFromCmdLineFlags(
app,
"",
globalNetworkFlags,
true,
false,
networkOption,
"",
)
if err != nil {
return err
}
if network.ClusterName != "" {
clusterNameFlagValue = network.ClusterName
network = models.ConvertClusterToNetwork(network)
}
if len(args) == 0 {
sc, _, err = importBlockchain(network, addValidatorFlags.RPC, ids.Empty, ux.Logger.PrintToUser)
if err != nil {
return err
}
}
if err := preAddChecks(args); err != nil {
return err
}
if sc.Networks[network.Name()].ClusterName != "" {
clusterNameFlagValue = sc.Networks[network.Name()].ClusterName
}
// TODO: will estimate fee in subsecuent PR
fee := uint64(0)
kc, err := keychain.GetKeychainFromCmdLineFlags(
app,
"to pay for transaction fees on P-Chain",
network,
keyName,
useEwoq,
useLedger,
ledgerAddresses,
fee,
)
if err != nil {
return err
}
sovereign := sc.Sovereign
if nodeEndpoint != "" {
nodeIDStr, publicKey, pop, err = utils.GetNodeID(nodeEndpoint)
if err != nil {
return err
}
}
if sovereign {
if !cmd.Flags().Changed(validatorWeightFlag) {
weight, err = app.Prompt.CaptureWeight(
"What weight would you like to assign to the validator?",
func(uint64) error { return nil },
)
if err != nil {
return err
}
}
}
// if we don't have a nodeID or ProofOfPossession by this point, prompt user if we want to add additional local node
if (!sovereign && nodeIDStr == "") || (sovereign && !createLocalValidator && nodeIDStr == "" && publicKey == "" && pop == "") {
if len(args) == 0 {
createLocalValidator = false
} else {
for {
local := "Use my local machine to spin up an additional validator"
existing := "I have an existing Lux node (we will require its NodeID and BLS info)"
if option, err := app.Prompt.CaptureList(
"How would you like to set up the new validator",
[]string{local, existing},
); err != nil {
return err
} else {
createLocalValidator = option == local
break
}
}
}
}
subnetID := sc.Networks[network.Name()].SubnetID
// if user chose to upsize a local node to add another local validator
var localValidatorClusterName string
if createLocalValidator {
localValidatorClusterName = localnet.LocalClusterName(network, blockchainName)
node, err := localnet.AddNodeToLocalCluster(app, ux.Logger.PrintToUser, localValidatorClusterName, httpPort, stakingPort)
if err != nil {
return err
}
nodeIDStr, publicKey, pop, err = utils.GetNodeID(node.URI)
if err != nil {
return err
}
sc, err = app.AddDefaultBlockchainRPCsToSidecar(blockchainName, network, []string{node.URI})
if err != nil {
return err
}
}
if nodeIDStr == "" {
nodeID, err := PromptNodeID("add as a blockchain validator")
if err != nil {
return err
}
nodeIDStr = nodeID.String()
}
if err := prompts.ValidateNodeID(nodeIDStr); err != nil {
return err
}
if sovereign && publicKey == "" && pop == "" {
publicKey, pop, err = promptProofOfPossession(true, true)
if err != nil {
return err
}
}
network.HandlePublicNetworkSimulation()
if !sovereign {
if err := UpdateKeychainWithSubnetControlKeys(kc, network, blockchainName); err != nil {
return err
}
}
deployer := subnet.NewPublicDeployer(app, kc, network)
if !sovereign {
return CallAddValidatorNonSOV(deployer, network, kc, useLedger, blockchainName, nodeIDStr, defaultValidatorParams, waitForTxAcceptance)
}
if err := CallAddValidator(
deployer,
network,
kc,
blockchainName,
subnetID,
nodeIDStr,
publicKey,
pop,
weight,
balanceLUX,
remainingBalanceOwnerAddr,
disableOwnerAddr,
sc,
addValidatorFlags.RPC,
); err != nil {
return err
}
if createLocalValidator && network.Kind == models.Local {
// For all blockchains validated by the cluster, set up an alias from blockchain name
// into blockchain id, to be mainly used in the blockchain RPC
return localnet.RefreshLocalClusterAliases(app, localValidatorClusterName)
}
return nil
}
func promptValidatorBalanceLUX(availableBalance float64) (float64, error) {
ux.Logger.PrintToUser("Validator's balance is used to pay for continuous fee to the P-Chain")
ux.Logger.PrintToUser("When this Balance reaches 0, the validator will be considered inactive and will no longer participate in validating the L1")
txt := "What balance would you like to assign to the validator (in LUX)?"
return app.Prompt.CaptureValidatorBalance(txt, availableBalance, constants.BootstrapValidatorBalanceLUX)
}
func CallAddValidator(
deployer *subnet.PublicDeployer,
network models.Network,
kc *keychain.Keychain,
blockchainName string,
subnetID ids.ID,
nodeIDStr string,
publicKey string,
pop string,
weight uint64,
balanceLUX float64,
remainingBalanceOwnerAddr string,
disableOwnerAddr string,
sc models.Sidecar,
rpcURL string,
) error {
nodeID, err := ids.NodeIDFromString(nodeIDStr)
if err != nil {
return err
}
blsInfo, err := blockchain.ConvertToBLSProofOfPossession(publicKey, pop)
if err != nil {
return fmt.Errorf("failure parsing BLS info: %w", err)
}
blockchainTimestamp, err := blockchain.GetBlockchainTimestamp(network)
if err != nil {
return fmt.Errorf("failed to get blockchain timestamp: %w", err)
}
expiry := uint64(blockchainTimestamp.Add(constants.DefaultValidationIDExpiryDuration).Unix())
chainSpec := contract.ChainSpec{
BlockchainName: blockchainName,
}
if sc.Networks[network.Name()].BlockchainID.String() != "" {
chainSpec.BlockchainID = sc.Networks[network.Name()].BlockchainID.String()
}
if sc.Networks[network.Name()].ValidatorManagerAddress == "" {
return fmt.Errorf("unable to find Validator Manager address")
}
validatorManagerAddress = sc.Networks[network.Name()].ValidatorManagerAddress
if validatorManagerOwner == "" {
validatorManagerOwner = sc.ValidatorManagerOwner
}
var ownerPrivateKey string
if !externalValidatorManagerOwner {
var ownerPrivateKeyFound bool
ownerPrivateKeyFound, _, _, ownerPrivateKey, err = contract.SearchForManagedKey(
app,
network,
common.HexToAddress(validatorManagerOwner),
true,
)
if err != nil {
return err
}
if !ownerPrivateKeyFound {
return fmt.Errorf("private key for Validator manager owner %s is not found", validatorManagerOwner)
}
}
pos := sc.PoS()
if pos {
// should take input prior to here for delegation fee, and min stake duration
if duration == 0 {
duration, err = PromptDuration(time.Now(), network, true) // it's pos
if err != nil {
return nil
}
}
if rewardsRecipientAddr == "" {
rewardsRecipientAddr, err = prompts.PromptAddress(
app.Prompt,
"receive the validation rewards",
app.GetKeyDir(),
app.GetKey,
"",
network,
prompts.EVMFormat,
"Address",
)
if err != nil {
return err
}
}
}
if sc.UseACP99 {
ux.Logger.PrintToUser(logging.Yellow.Wrap("Validator Manager Protocol: V2"))
} else {
ux.Logger.PrintToUser(logging.Yellow.Wrap("Validator Manager Protocol: v1.0.0"))
}
ux.Logger.PrintToUser(logging.Yellow.Wrap("Validation manager owner %s pays for the initialization of the validator's registration (Blockchain gas token)"), validatorManagerOwner)
if rpcURL == "" {
rpcURL, _, err = contract.GetBlockchainEndpoints(
app,
network,
chainSpec,
true,
false,
)
if err != nil {
return err
}
}
ux.Logger.PrintToUser(logging.Yellow.Wrap("RPC Endpoint: %s"), rpcURL)
totalWeight, err := validator.GetTotalWeight(network.SDKNetwork(), subnetID)
if err != nil {
return err
}
allowedChange := float64(totalWeight) * constants.MaxL1TotalWeightChange
if float64(weight) > allowedChange {
return fmt.Errorf("can't make change: desired validator weight %d exceeds max allowed weight change of %d", newWeight, uint64(allowedChange))
}
if balanceLUX == 0 {
availableBalance, err := utils.GetNetworkBalance(kc.Addresses().List(), network.Endpoint)
if err != nil {
return err
}
if availableBalance == 0 {
return fmt.Errorf("chosen key has zero balance")
}
balanceLUX, err = promptValidatorBalanceLUX(float64(availableBalance) / float64(units.Lux))
if err != nil {
return err
}
}
// convert to nanoLUX
balance := uint64(balanceLUX * float64(units.Lux))
if remainingBalanceOwnerAddr == "" {
remainingBalanceOwnerAddr, err = blockchain.GetKeyForChangeOwner(app, network)
if err != nil {
return err
}
}
remainingBalanceOwnerAddrID, err := address.ParseToIDs([]string{remainingBalanceOwnerAddr})
if err != nil {
return fmt.Errorf("failure parsing remaining balanche owner address %s: %w", remainingBalanceOwnerAddr, err)
}
remainingBalanceOwners := warpMessage.PChainOwner{
Threshold: 1,
Addresses: remainingBalanceOwnerAddrID,
}
if disableOwnerAddr == "" {
disableOwnerAddr, err = prompts.PromptAddress(
app.Prompt,
"be able to disable the validator using P-Chain transactions",
app.GetKeyDir(),
app.GetKey,
"",
network,
prompts.PChainFormat,
"Enter P-Chain address (Example: P-...)",
)
if err != nil {
return err
}
}
disableOwnerAddrID, err := address.ParseToIDs([]string{disableOwnerAddr})
if err != nil {
return fmt.Errorf("failure parsing disable owner address %s: %w", disableOwnerAddr, err)
}
disableOwners := warpMessage.PChainOwner{
Threshold: 1,
Addresses: disableOwnerAddrID,
}
extraAggregatorPeers, err := blockchain.GetAggregatorExtraPeers(app, clusterNameFlagValue)
if err != nil {
return err
}
aggregatorLogger, err := signatureaggregator.NewSignatureAggregatorLogger(
addValidatorFlags.SigAggFlags.AggregatorLogLevel,
addValidatorFlags.SigAggFlags.AggregatorLogToStdout,
app.GetAggregatorLogDir(clusterNameFlagValue),
)
if err != nil {
return err
}
if err = signatureaggregator.UpdateSignatureAggregatorPeers(app, network, extraAggregatorPeers, aggregatorLogger); err != nil {
return err
}
aggregatorCtx, aggregatorCancel := sdkutils.GetTimedContext(constants.SignatureAggregatorTimeout)
defer aggregatorCancel()
signatureAggregatorEndpoint, err := signatureaggregator.GetSignatureAggregatorEndpoint(app, network)
if err != nil {
return err
}
signedMessage, validationID, rawTx, err := validatormanager.InitValidatorRegistration(
aggregatorCtx,
app,
network,
rpcURL,
chainSpec,
externalValidatorManagerOwner,
validatorManagerOwner,
ownerPrivateKey,
nodeID,
blsInfo.PublicKey[:],
expiry,
remainingBalanceOwners,
disableOwners,
weight,
aggregatorLogger,
pos,
delegationFee,
duration,
common.HexToAddress(rewardsRecipientAddr),
validatorManagerAddress,
sc.UseACP99,
initiateTxHash,
signatureAggregatorEndpoint,
)
if err != nil {
return err
}
if rawTx != nil {
dump, err := evm.TxDump("Initializing Validator Registration", rawTx)
if err == nil {
ux.Logger.PrintToUser(dump)
}
return err
}
ux.Logger.PrintToUser("ValidationID: %s", validationID)
txID, _, err := deployer.RegisterL1Validator(balance, blsInfo, signedMessage)
if err != nil {
if !strings.Contains(err.Error(), "warp message already issued for validationID") {
return err
}
ux.Logger.PrintToUser(logging.LightBlue.Wrap("The Validation ID was already registered on the P-Chain. Proceeding to the next step"))
} else {
ux.Logger.PrintToUser("RegisterL1ValidatorTx ID: %s", txID)
if err := blockchain.UpdatePChainHeight(
"Waiting for P-Chain to update validator information ...",
); err != nil {
return err
}
}
aggregatorCtx, aggregatorCancel = sdkutils.GetTimedContext(constants.SignatureAggregatorTimeout)
defer aggregatorCancel()
rawTx, err = validatormanager.FinishValidatorRegistration(
aggregatorCtx,
app,
network,
rpcURL,
chainSpec,
externalValidatorManagerOwner,
validatorManagerOwner,
ownerPrivateKey,
validationID,
aggregatorLogger,
validatorManagerAddress,
signatureAggregatorEndpoint,
)
if err != nil {
return err
}
if rawTx != nil {
dump, err := evm.TxDump("Finish Validator Registration", rawTx)
if err == nil {
ux.Logger.PrintToUser(dump)
}
return err
}
ux.Logger.PrintToUser(" NodeID: %s", nodeID)
ux.Logger.PrintToUser(" Network: %s", network.Name())
// weight is inaccurate for PoS as it's fetched during registration
if !pos {
ux.Logger.PrintToUser(" Weight: %d", weight)
}
ux.Logger.PrintToUser(" Balance: %.2f", balanceLUX)
ux.Logger.GreenCheckmarkToUser("Validator successfully added to the L1")
return nil
}
func CallAddValidatorNonSOV(
deployer *subnet.PublicDeployer,
network models.Network,
kc *keychain.Keychain,
useLedgerSetting bool,
blockchainName string,
nodeIDStr string,
defaultValidatorParamsSetting bool,
waitForTxAcceptanceSetting bool,
) error {
var start time.Time
nodeID, err := ids.NodeIDFromString(nodeIDStr)
if err != nil {
return err
}
useLedger = useLedgerSetting
defaultValidatorParams = defaultValidatorParamsSetting
waitForTxAcceptance = waitForTxAcceptanceSetting
if defaultValidatorParams {
useDefaultDuration = true
useDefaultStartTime = true
useDefaultWeight = true
}
if useDefaultDuration && duration != 0 {
return errMutuallyExclusiveDurationOptions
}
if useDefaultStartTime && startTimeStr != "" {
return errMutuallyExclusiveStartOptions
}
if useDefaultWeight && weight != 0 {
return errMutuallyExclusiveWeightOptions
}
if outputTxPath != "" {
if utils.FileExists(outputTxPath) {
return fmt.Errorf("outputTxPath %q already exists", outputTxPath)
}
}
_, err = ValidateSubnetNameAndGetChains([]string{blockchainName})
if err != nil {
return err
}
sc, err := app.LoadSidecar(blockchainName)
if err != nil {
return err
}
subnetID := sc.Networks[network.Name()].SubnetID
if subnetID == ids.Empty {
return errNoSubnetID
}
isPermissioned, controlKeys, threshold, err := txutils.GetOwners(network, subnetID)
if err != nil {
return err
}
if !isPermissioned {
return ErrNotPermissionedSubnet
}
kcKeys, err := kc.PChainFormattedStrAddresses()
if err != nil {
return err
}
// get keys for add validator tx signing
if subnetAuthKeys != nil {
if err := prompts.CheckSubnetAuthKeys(kcKeys, subnetAuthKeys, controlKeys, threshold); err != nil {
return err
}
} else {
subnetAuthKeys, err = prompts.GetSubnetAuthKeys(app.Prompt, kcKeys, controlKeys, threshold)
if err != nil {
return err
}
}
ux.Logger.PrintToUser("Your auth keys for add validator tx creation: %s", subnetAuthKeys)
selectedWeight, err := getWeight()
if err != nil {
return err
}
if selectedWeight < constants.MinStakeWeight {
return fmt.Errorf("invalid weight, must be greater than or equal to %d: %d", constants.MinStakeWeight, selectedWeight)
}
start, selectedDuration, err := getTimeParameters(network, nodeID, true)
if err != nil {
return err
}
ux.Logger.PrintToUser("NodeID: %s", nodeID.String())
ux.Logger.PrintToUser("Network: %s", network.Name())
ux.Logger.PrintToUser("Start time: %s", start.Format(constants.TimeParseLayout))
ux.Logger.PrintToUser("End time: %s", start.Add(selectedDuration).Format(constants.TimeParseLayout))
ux.Logger.PrintToUser("Weight: %d", selectedWeight)
ux.Logger.PrintToUser("Inputs complete, issuing transaction to add the provided validator information...")
isFullySigned, tx, remainingSubnetAuthKeys, err := deployer.AddValidatorNonSOV(
waitForTxAcceptance,
controlKeys,
subnetAuthKeys,
subnetID,
nodeID,
selectedWeight,
start,
selectedDuration,
)
if err != nil {
return err
}
if !isFullySigned {
if err := SaveNotFullySignedTx(
"Add Validator",
tx,
blockchainName,
subnetAuthKeys,
remainingSubnetAuthKeys,
outputTxPath,
false,
); err != nil {
return err
}
}
return err
}
func PromptDuration(start time.Time, network models.Network, isPos bool) (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\""
var d time.Duration
var err error
switch {
case network.Kind == models.Testnet:
d, err = app.Prompt.CaptureTestnetDuration(txt)
case network.Kind == models.Mainnet && isPos:
d, err = app.Prompt.CaptureMainnetL1StakingDuration(txt)
case network.Kind == models.Mainnet && !isPos:
d, err = app.Prompt.CaptureMainnetDuration(txt)
default:
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 getTimeParameters(network models.Network, nodeID ids.NodeID, isValidator bool) (time.Time, time.Duration, error) {
defaultStakingStartLeadTime := constants.StakingStartLeadTime
if network.Kind == models.Devnet {
defaultStakingStartLeadTime = constants.DevnetStakingStartLeadTime
}
const custom = "Custom"
// this sets either the global var startTimeStr or useDefaultStartTime to enable repeated execution with
// state keeping from node cmds
if startTimeStr == "" && !useDefaultStartTime {
if isValidator {
ux.Logger.PrintToUser("When should your validator start validating?\n" +
"If you validator is not ready by this time, subnet downtime can occur.")
} else {
ux.Logger.PrintToUser("When do you want to start delegating?\n")
}
defaultStartOption := "Start in " + ux.FormatDuration(defaultStakingStartLeadTime)
startTimeOptions := []string{defaultStartOption, custom}
startTimeOption, err := app.Prompt.CaptureList("Start time", startTimeOptions)
if err != nil {
return time.Time{}, 0, err
}
switch startTimeOption {
case defaultStartOption:
useDefaultStartTime = true
default:
start, err := promptStart()
if err != nil {
return time.Time{}, 0, err
}
startTimeStr = start.Format(constants.TimeParseLayout)
}
}
var (
err error
start time.Time
)
if startTimeStr != "" {
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)
}
} else {
start = time.Now().Add(defaultStakingStartLeadTime)
}
// this sets either the global var duration or useDefaultDuration to enable repeated execution with
// state keeping from node cmds
if duration == 0 && !useDefaultDuration {
msg := "How long should your validator validate for?"
if !isValidator {
msg = "How long do you want to delegate for?"
}
const defaultDurationOption = "Until primary network validator expires"
durationOptions := []string{defaultDurationOption, custom}
durationOption, err := app.Prompt.CaptureList(msg, durationOptions)
if err != nil {
return time.Time{}, 0, err
}
switch durationOption {
case defaultDurationOption:
useDefaultDuration = true
default:
duration, err = PromptDuration(start, network, false) // notSoV
if err != nil {
return time.Time{}, 0, err
}
}
}
var selectedDuration time.Duration
if useDefaultDuration {
// avoid setting both globals useDefaultDuration and duration
selectedDuration, err = utils.GetRemainingValidationTime(network.Endpoint, nodeID, luxdconstants.PrimaryNetworkID, start)
if err != nil {
return time.Time{}, 0, err
}
} else {
selectedDuration = duration
}
return start, selectedDuration, 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(goal string) (ids.NodeID, error) {
txt := fmt.Sprintf("What is the NodeID of the node you want to %s?", goal)
return app.Prompt.CaptureNodeID(txt)
}
func getWeight() (uint64, error) {
// this sets either the global var weight or useDefaultWeight to enable repeated execution with
// state keeping from node cmds
if weight == 0 && !useDefaultWeight {
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:
useDefaultWeight = true
default:
weight, err = app.Prompt.CaptureWeight(txt, func(uint64) error { return nil })
if err != nil {
return 0, err
}
}
}
if useDefaultWeight {
return constants.DefaultStakeWeight, nil
}
return weight, nil
}