mirror of
https://github.com/luxfi/cli.git
synced 2026-10-30 17:01:20 +00:00
512 lines
16 KiB
Go
512 lines
16 KiB
Go
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
package blockchaincmd
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
|
|
sdkutils "github.com/luxfi/cli/v2/sdk/utils"
|
|
|
|
"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/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"
|
|
validatorsdk "github.com/luxfi/cli/v2/sdk/validator"
|
|
validatormanagerSDK "github.com/luxfi/cli/v2/sdk/validatormanager"
|
|
"github.com/luxfi/node/v2/v2/api/info"
|
|
"github.com/luxfi/ids"
|
|
"github.com/luxfi/node/v2/v2/utils/logging"
|
|
|
|
"github.com/ethereum/go-ethereum/common"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
var (
|
|
uptimeSec uint64
|
|
force bool
|
|
removeValidatorFlags BlockchainRemoveValidatorFlags
|
|
)
|
|
|
|
type BlockchainRemoveValidatorFlags struct {
|
|
RPC string
|
|
SigAggFlags flags.SignatureAggregatorFlags
|
|
}
|
|
|
|
// lux blockchain removeValidator
|
|
func newRemoveValidatorCmd() *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "removeValidator [blockchainName]",
|
|
Short: "Remove a permissioned validator from your blockchain",
|
|
Long: `The blockchain removeValidator command stops a whitelisted blockchain network validator from
|
|
validating your deployed Blockchain.
|
|
|
|
To remove the validator from the Subnet's allow list, provide the validator's unique NodeID. You can bypass
|
|
these prompts by providing the values with flags.`,
|
|
RunE: removeValidator,
|
|
PreRunE: cobrautils.ExactArgs(1),
|
|
}
|
|
networkoptions.AddNetworkFlagsToCmd(cmd, &globalNetworkFlags, false, networkoptions.DefaultSupportedNetworkOptions)
|
|
flags.AddRPCFlagToCmd(cmd, app, &removeValidatorFlags.RPC)
|
|
sigAggGroup := flags.AddSignatureAggregatorFlagsToCmd(cmd, &removeValidatorFlags.SigAggFlags)
|
|
cmd.Flags().StringVarP(&keyName, "key", "k", "", "select the key to use [testnet deploy only]")
|
|
cmd.Flags().StringSliceVar(&subnetAuthKeys, "auth-keys", nil, "(for non-SOV blockchain only) control keys that will be used to authenticate the removeValidator tx")
|
|
cmd.Flags().StringVar(&outputTxPath, "output-tx-path", "", "(for non-SOV blockchain only) file path of the removeValidator 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")
|
|
cmd.Flags().StringVar(&nodeIDStr, "node-id", "", "node-id of the validator")
|
|
cmd.Flags().StringVar(&nodeEndpoint, "node-endpoint", "", "remove validator that responds to the given endpoint")
|
|
cmd.Flags().Uint64Var(&uptimeSec, "uptime", 0, "validator's uptime in seconds. If not provided, it will be automatically calculated")
|
|
cmd.Flags().BoolVar(&force, "force", false, "force validator removal even if it's not getting rewarded")
|
|
cmd.Flags().BoolVar(&externalValidatorManagerOwner, "external-evm-signature", false, "set this value to true when signing validator manager tx outside of cli (for multisig or ledger)")
|
|
cmd.Flags().StringVar(&validatorManagerOwner, "validator-manager-owner", "", "force using this address to issue transactions to the validator manager")
|
|
cmd.Flags().StringVar(&initiateTxHash, "initiate-tx-hash", "", "initiate tx is already issued, with the given hash")
|
|
cmd.SetHelpFunc(flags.WithGroupedHelp([]flags.GroupedFlags{sigAggGroup}))
|
|
return cmd
|
|
}
|
|
|
|
func removeValidator(_ *cobra.Command, args []string) error {
|
|
blockchainName := args[0]
|
|
_, err := ValidateSubnetNameAndGetChains([]string{blockchainName})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
sc, err := app.LoadSidecar(blockchainName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
network, err := networkoptions.GetNetworkFromCmdLineFlags(
|
|
app,
|
|
"",
|
|
globalNetworkFlags,
|
|
true,
|
|
false,
|
|
networkoptions.GetNetworkFromSidecar(sc, networkoptions.DefaultSupportedNetworkOptions),
|
|
"",
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if network.ClusterName != "" {
|
|
network = models.ConvertClusterToNetwork(network)
|
|
}
|
|
|
|
// 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
|
|
}
|
|
network.HandlePublicNetworkSimulation()
|
|
|
|
scNetwork := sc.Networks[network.Name()]
|
|
subnetID := scNetwork.SubnetID
|
|
if subnetID == ids.Empty {
|
|
return constants.ErrNoSubnetID
|
|
}
|
|
|
|
var nodeID ids.NodeID
|
|
switch {
|
|
case nodeEndpoint != "":
|
|
infoClient := info.NewClient(nodeEndpoint)
|
|
ctx, cancel := utils.GetAPILargeContext()
|
|
defer cancel()
|
|
nodeID, _, err = infoClient.GetNodeID(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
case nodeIDStr == "":
|
|
nodeID, err = PromptNodeID("remove as a blockchain validator")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
default:
|
|
nodeID, err = ids.NodeIDFromString(nodeIDStr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if sc.Sovereign && removeValidatorFlags.RPC == "" {
|
|
removeValidatorFlags.RPC, _, err = contract.GetBlockchainEndpoints(
|
|
app,
|
|
network,
|
|
contract.ChainSpec{
|
|
BlockchainName: blockchainName,
|
|
},
|
|
true,
|
|
false,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
validatorKind, err := validatorsdk.GetValidatorKind(network.SDKNetwork(), subnetID, nodeID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if validatorKind == validatorsdk.NonValidator {
|
|
// it may be unregistered from P-Chain, but registered on validator manager
|
|
// due to a previous partial removal operation
|
|
validatorManagerAddress = sc.Networks[network.Name()].ValidatorManagerAddress
|
|
validationID, err := validatorsdk.GetValidationID(
|
|
removeValidatorFlags.RPC,
|
|
common.HexToAddress(validatorManagerAddress),
|
|
nodeID,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if validationID != ids.Empty {
|
|
validatorKind = validatorsdk.SovereignValidator
|
|
}
|
|
}
|
|
if validatorKind == validatorsdk.NonValidator {
|
|
return fmt.Errorf("node %s is not a validator of subnet %s on %s", nodeID, subnetID, network.Name())
|
|
}
|
|
|
|
if validatorKind == validatorsdk.SovereignValidator {
|
|
if outputTxPath != "" {
|
|
return errors.New("--output-tx-path flag cannot be used for non-SOV (Subnet-Only Validators) blockchains")
|
|
}
|
|
|
|
if len(subnetAuthKeys) > 0 {
|
|
return errors.New("--subnetAuthKeys flag cannot be used for non-SOV (Subnet-Only Validators) blockchains")
|
|
}
|
|
}
|
|
if outputTxPath != "" {
|
|
if _, err := os.Stat(outputTxPath); err == nil {
|
|
return fmt.Errorf("outputTxPath %q already exists", outputTxPath)
|
|
}
|
|
}
|
|
|
|
deployer := subnet.NewPublicDeployer(app, kc, network)
|
|
if validatorKind == validatorsdk.NonSovereignValidator {
|
|
isValidator, err := subnet.IsSubnetValidator(subnetID, nodeID, network)
|
|
if err != nil {
|
|
// just warn the user, don't fail
|
|
ux.Logger.PrintToUser("failed to check if node is a validator on the subnet: %s", err)
|
|
} else if !isValidator {
|
|
// this is actually an error
|
|
return fmt.Errorf("node %s is not a validator on subnet %s", nodeID, subnetID)
|
|
}
|
|
if err := UpdateKeychainWithSubnetControlKeys(kc, network, blockchainName); err != nil {
|
|
return err
|
|
}
|
|
return removeValidatorNonSOV(deployer, network, subnetID, kc, blockchainName, nodeID)
|
|
}
|
|
if err := removeValidatorSOV(
|
|
deployer,
|
|
network,
|
|
blockchainName,
|
|
nodeID,
|
|
uptimeSec,
|
|
isBootstrapValidatorForNetwork(nodeID, scNetwork),
|
|
force,
|
|
removeValidatorFlags.RPC,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
// remove the validator from the list of bootstrap validators
|
|
newBootstrapValidators := utils.Filter(scNetwork.BootstrapValidators, func(b models.SubnetValidator) bool {
|
|
if id, _ := ids.NodeIDFromString(b.NodeID); id != nodeID {
|
|
return true
|
|
}
|
|
return false
|
|
})
|
|
// save new bootstrap validators and save sidecar
|
|
scNetwork.BootstrapValidators = newBootstrapValidators
|
|
sc.Networks[network.Name()] = scNetwork
|
|
if err := app.UpdateSidecar(&sc); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func isBootstrapValidatorForNetwork(nodeID ids.NodeID, scNetwork models.NetworkData) bool {
|
|
filteredBootstrapValidators := utils.Filter(scNetwork.BootstrapValidators, func(b models.SubnetValidator) bool {
|
|
if id, err := ids.NodeIDFromString(b.NodeID); err == nil && id == nodeID {
|
|
return true
|
|
}
|
|
return false
|
|
})
|
|
return len(filteredBootstrapValidators) > 0
|
|
}
|
|
|
|
func removeValidatorSOV(
|
|
deployer *subnet.PublicDeployer,
|
|
network models.Network,
|
|
blockchainName string,
|
|
nodeID ids.NodeID,
|
|
uptimeSec uint64,
|
|
isBootstrapValidator bool,
|
|
force bool,
|
|
rpcURL string,
|
|
) error {
|
|
chainSpec := contract.ChainSpec{
|
|
BlockchainName: blockchainName,
|
|
}
|
|
|
|
sc, err := app.LoadSidecar(chainSpec.BlockchainName)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to load sidecar: %w", err)
|
|
}
|
|
|
|
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("not private key found for Validator manager owner %s", validatorManagerOwner)
|
|
}
|
|
}
|
|
|
|
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("Validator manager owner %s pays for the initialization of the validator's removal (Blockchain gas token)"), validatorManagerOwner)
|
|
|
|
if sc.Networks[network.Name()].ValidatorManagerAddress == "" {
|
|
return fmt.Errorf("unable to find Validator Manager address")
|
|
}
|
|
validatorManagerAddress = sc.Networks[network.Name()].ValidatorManagerAddress
|
|
|
|
ux.Logger.PrintToUser(logging.Yellow.Wrap("RPC Endpoint: %s"), rpcURL)
|
|
|
|
clusterName := sc.Networks[network.Name()].ClusterName
|
|
extraAggregatorPeers, err := blockchain.GetAggregatorExtraPeers(app, clusterName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
aggregatorLogger, err := signatureaggregator.NewSignatureAggregatorLogger(
|
|
removeValidatorFlags.SigAggFlags.AggregatorLogLevel,
|
|
removeValidatorFlags.SigAggFlags.AggregatorLogToStdout,
|
|
app.GetAggregatorLogDir(clusterName),
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if force && sc.PoS() {
|
|
ux.Logger.PrintToUser(logging.Yellow.Wrap("Forcing removal of %s as it is a PoS bootstrap validator"), nodeID)
|
|
}
|
|
|
|
if err = signatureaggregator.UpdateSignatureAggregatorPeers(app, network, extraAggregatorPeers, aggregatorLogger); err != nil {
|
|
return err
|
|
}
|
|
signatureAggregatorEndpoint, err := signatureaggregator.GetSignatureAggregatorEndpoint(app, network)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
aggregatorCtx, aggregatorCancel := sdkutils.GetTimedContext(constants.SignatureAggregatorTimeout)
|
|
defer aggregatorCancel()
|
|
// try to remove the validator. If err is "delegator ineligible for rewards" confirm with user and force remove
|
|
signedMessage, validationID, rawTx, err := validatormanager.InitValidatorRemoval(
|
|
aggregatorCtx,
|
|
app,
|
|
network,
|
|
rpcURL,
|
|
chainSpec,
|
|
externalValidatorManagerOwner,
|
|
validatorManagerOwner,
|
|
ownerPrivateKey,
|
|
nodeID,
|
|
aggregatorLogger,
|
|
sc.PoS(),
|
|
uptimeSec,
|
|
isBootstrapValidator || force,
|
|
validatorManagerAddress,
|
|
sc.UseACP99,
|
|
initiateTxHash,
|
|
signatureAggregatorEndpoint,
|
|
)
|
|
if err != nil && errors.Is(err, validatormanagerSDK.ErrValidatorIneligibleForRewards) {
|
|
ux.Logger.PrintToUser("Calculated rewards is zero. Validator %s is not eligible for rewards", nodeID)
|
|
force, err = app.Prompt.CaptureNoYes("Do you want to continue with validator removal?")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !force {
|
|
return fmt.Errorf("validator %s is not eligible for rewards. Use --force flag to force removal", nodeID)
|
|
}
|
|
aggregatorCtx, aggregatorCancel = sdkutils.GetTimedContext(constants.SignatureAggregatorTimeout)
|
|
defer aggregatorCancel()
|
|
signedMessage, validationID, _, err = validatormanager.InitValidatorRemoval(
|
|
aggregatorCtx,
|
|
app,
|
|
network,
|
|
rpcURL,
|
|
chainSpec,
|
|
externalValidatorManagerOwner,
|
|
validatorManagerOwner,
|
|
ownerPrivateKey,
|
|
nodeID,
|
|
aggregatorLogger,
|
|
sc.PoS(),
|
|
uptimeSec,
|
|
true, // force
|
|
validatorManagerAddress,
|
|
sc.UseACP99,
|
|
initiateTxHash,
|
|
signatureAggregatorEndpoint,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else if err != nil {
|
|
return err
|
|
}
|
|
if rawTx != nil {
|
|
dump, err := evm.TxDump("Initializing Validator Removal", rawTx)
|
|
if err == nil {
|
|
ux.Logger.PrintToUser(dump)
|
|
}
|
|
return err
|
|
}
|
|
|
|
ux.Logger.PrintToUser("ValidationID: %s", validationID)
|
|
txID, _, err := deployer.SetL1ValidatorWeight(signedMessage)
|
|
if err != nil {
|
|
if !strings.Contains(err.Error(), "could not load L1 validator: not found") {
|
|
return err
|
|
}
|
|
ux.Logger.PrintToUser(logging.LightBlue.Wrap("The Validation ID was already removed on the P-Chain. Proceeding to the next step"))
|
|
} else {
|
|
ux.Logger.PrintToUser("SetL1ValidatorWeightTx 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.FinishValidatorRemoval(
|
|
aggregatorCtx,
|
|
app,
|
|
network,
|
|
rpcURL,
|
|
chainSpec,
|
|
externalValidatorManagerOwner,
|
|
validatorManagerOwner,
|
|
ownerPrivateKey,
|
|
validationID,
|
|
aggregatorLogger,
|
|
validatorManagerAddress,
|
|
sc.UseACP99,
|
|
signatureAggregatorEndpoint,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if rawTx != nil {
|
|
dump, err := evm.TxDump("Finish Validator Removal", rawTx)
|
|
if err == nil {
|
|
ux.Logger.PrintToUser(dump)
|
|
}
|
|
return err
|
|
}
|
|
|
|
ux.Logger.GreenCheckmarkToUser("Validator successfully removed from the Subnet")
|
|
|
|
return nil
|
|
}
|
|
|
|
func removeValidatorNonSOV(deployer *subnet.PublicDeployer, network models.Network, subnetID ids.ID, kc *keychain.Keychain, blockchainName string, nodeID ids.NodeID) error {
|
|
_, controlKeys, threshold, err := txutils.GetOwners(network, subnetID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// add control keys to the keychain whenever possible
|
|
if err := kc.AddAddresses(controlKeys); err != nil {
|
|
return err
|
|
}
|
|
|
|
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 remove validator tx creation: %s", subnetAuthKeys)
|
|
|
|
ux.Logger.PrintToUser("NodeID: %s", nodeID.String())
|
|
ux.Logger.PrintToUser("Network: %s", network.Name())
|
|
ux.Logger.PrintToUser("Inputs complete, issuing transaction to remove the specified validator...")
|
|
|
|
isFullySigned, tx, remainingSubnetAuthKeys, err := deployer.RemoveValidator(
|
|
controlKeys,
|
|
subnetAuthKeys,
|
|
subnetID,
|
|
nodeID,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !isFullySigned {
|
|
if err := SaveNotFullySignedTx(
|
|
"Remove Validator",
|
|
tx,
|
|
blockchainName,
|
|
subnetAuthKeys,
|
|
remainingSubnetAuthKeys,
|
|
outputTxPath,
|
|
false,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return err
|
|
}
|