Files

480 lines
14 KiB
Go

// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package blockchaincmd
import (
"fmt"
"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/key"
"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/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"
"github.com/luxfi/crypto/bls"
"github.com/luxfi/node/v2/v2/utils/formatting"
"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"
"github.com/luxfi/node/v2/v2/vms/platformvm"
"github.com/ethereum/go-ethereum/common"
"github.com/spf13/cobra"
)
var (
newWeight uint64
initiateTxHash string
changeWeightFlags BlockchainChangeWeightFlags
)
type BlockchainChangeWeightFlags struct {
RPC string
SigAggFlags flags.SignatureAggregatorFlags
}
// lux blockchain addValidator
func newChangeWeightCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "changeWeight [blockchainName]",
Short: "Changes the weight of a L1 validator",
Long: `The blockchain changeWeight command changes the weight of a L1 Validator.
The L1 has to be a Proof of Authority L1.`,
RunE: setWeight,
PreRunE: cobrautils.ExactArgs(1),
}
networkoptions.AddNetworkFlagsToCmd(cmd, &globalNetworkFlags, true, networkoptions.DefaultSupportedNetworkOptions)
flags.AddRPCFlagToCmd(cmd, app, &changeWeightFlags.RPC)
sigAggGroup := flags.AddSignatureAggregatorFlagsToCmd(cmd, &changeWeightFlags.SigAggFlags)
cmd.Flags().StringVarP(&keyName, "key", "k", "", "select the key to use [testnet/devnet only]")
cmd.Flags().Uint64Var(&newWeight, "weight", 0, "set the new staking weight of the validator")
cmd.Flags().BoolVarP(&useEwoq, "ewoq", "e", false, "use ewoq key [testnet/devnet only]")
cmd.Flags().StringVar(&nodeIDStr, "node-id", "", "node-id of the validator")
cmd.Flags().StringVar(&nodeEndpoint, "node-endpoint", "", "gather node id/bls from publicly available luxd apis on the given endpoint")
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().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 setWeight(_ *cobra.Command, args []string) error {
blockchainName := args[0]
sc, err := app.LoadSidecar(blockchainName)
if err != nil {
return fmt.Errorf("failed to load sidecar: %w", err)
}
networkOptionsList := networkoptions.GetNetworkFromSidecar(sc, networkoptions.DefaultSupportedNetworkOptions)
network, err := networkoptions.GetNetworkFromCmdLineFlags(
app,
"",
globalNetworkFlags,
true,
false,
networkOptionsList,
"",
)
if err != nil {
return err
}
// 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()
subnetID := sc.Networks[network.Name()].SubnetID
if subnetID == ids.Empty {
return errNoSubnetID
}
if nodeEndpoint != "" {
nodeIDStr, publicKey, pop, err = utils.GetNodeID(nodeEndpoint)
if err != nil {
return err
}
}
var nodeID ids.NodeID
if nodeIDStr == "" {
nodeID, err = PromptNodeID("change weight")
if err != nil {
return err
}
} else {
if err := prompts.ValidateNodeID(nodeIDStr); err != nil {
return err
}
nodeID, err = ids.NodeIDFromString(nodeIDStr)
if err != nil {
return err
}
}
chainSpec := contract.ChainSpec{
BlockchainName: blockchainName,
}
if changeWeightFlags.RPC == "" {
changeWeightFlags.RPC, _, err = contract.GetBlockchainEndpoints(
app,
network,
chainSpec,
true,
false,
)
if err != nil {
return err
}
}
if sc.Networks[network.Name()].ValidatorManagerAddress == "" {
return fmt.Errorf("unable to find Validator Manager address")
}
validatorManagerAddress = sc.Networks[network.Name()].ValidatorManagerAddress
validationID, err := validator.GetValidationID(changeWeightFlags.RPC, common.HexToAddress(validatorManagerAddress), nodeID)
if err != nil {
return err
}
if validationID == ids.Empty {
return fmt.Errorf("node %s is not a L1 validator", nodeID)
}
ux.Logger.PrintToUser("ValidationID: %s", validationID)
var validatorInfo platformvm.L1Validator
if initiateTxHash == "" {
validatorInfo, err = validator.GetValidatorInfo(network.SDKNetwork(), validationID)
if err != nil {
return err
}
totalWeight, err := validator.GetTotalWeight(network.SDKNetwork(), subnetID)
if err != nil {
return err
}
allowedChange := float64(totalWeight) * constants.MaxL1TotalWeightChange
allowedWeightFunction := func(v uint64) error {
delta := uint64(0)
if v > validatorInfo.Weight {
delta = v - validatorInfo.Weight
} else {
delta = validatorInfo.Weight - v
}
if delta > uint64(allowedChange) {
return fmt.Errorf("weight change %d exceeds max allowed weight change of %d", delta, uint64(allowedChange))
}
return nil
}
if !sc.UseACP99 {
if float64(validatorInfo.Weight) > allowedChange {
return fmt.Errorf("can't make change: current validator weight %d exceeds max allowed weight change of %d", validatorInfo.Weight, uint64(allowedChange))
}
allowedChange = float64(totalWeight-validatorInfo.Weight) * constants.MaxL1TotalWeightChange
allowedWeightFunction = func(v uint64) error {
if v > uint64(allowedChange) {
return fmt.Errorf("new weight exceeds max allowed weight change of %d", uint64(allowedChange))
}
return nil
}
}
if newWeight == 0 {
ux.Logger.PrintToUser("Current validator weight is %d", validatorInfo.Weight)
newWeight, err = app.Prompt.CaptureWeight(
"What weight would you like to assign to the validator?",
allowedWeightFunction,
)
if err != nil {
return err
}
}
if err := allowedWeightFunction(newWeight); err != nil {
return err
}
}
deployer := subnet.NewPublicDeployer(app, kc, network)
if sc.UseACP99 {
ux.Logger.PrintToUser(logging.Yellow.Wrap("Validator Manager Protocol: V2"))
return changeWeightACP99(
deployer,
network,
blockchainName,
nodeID,
newWeight,
initiateTxHash,
)
} else {
ux.Logger.PrintToUser(logging.Yellow.Wrap("Validator Manager Protocol: v1.0.0"))
}
publicKey, err = formatting.Encode(formatting.HexNC, bls.PublicKeyToCompressedBytes(validatorInfo.PublicKey))
if err != nil {
return err
}
if pop == "" {
_, pop, err = promptProofOfPossession(false, true)
if err != nil {
return err
}
}
var remainingBalanceOwnerAddr, disableOwnerAddr string
hrp := key.GetHRP(network.ID)
if validatorInfo.RemainingBalanceOwner != nil && len(validatorInfo.RemainingBalanceOwner.Addrs) > 0 {
remainingBalanceOwnerAddr, err = address.Format("P", hrp, validatorInfo.RemainingBalanceOwner.Addrs[0][:])
if err != nil {
return err
}
}
if validatorInfo.DeactivationOwner != nil && len(validatorInfo.DeactivationOwner.Addrs) > 0 {
disableOwnerAddr, err = address.Format("P", hrp, validatorInfo.DeactivationOwner.Addrs[0][:])
if err != nil {
return err
}
}
// first remove the validator from subnet
err = removeValidatorSOV(
deployer,
network,
blockchainName,
nodeID,
0, // automatic uptime
isBootstrapValidatorForNetwork(nodeID, sc.Networks[network.Name()]),
false, // don't force
changeWeightFlags.RPC,
)
if err != nil {
return err
}
balance := validatorInfo.Balance
if validatorInfo.RemainingBalanceOwner != nil && len(validatorInfo.RemainingBalanceOwner.Addrs) > 0 {
availableBalance, err := utils.GetNetworkBalance([]ids.ShortID{validatorInfo.RemainingBalanceOwner.Addrs[0]}, network.Endpoint)
if err != nil {
ux.Logger.RedXToUser("failure checking remaining balance of validator: %s. continuing with default value", err)
} else if availableBalance < balance {
balance = availableBalance
}
}
// add back validator to subnet with updated weight
return CallAddValidator(
deployer,
network,
kc,
blockchainName,
subnetID,
nodeID.String(),
publicKey,
pop,
newWeight,
float64(balance)/float64(units.Lux),
remainingBalanceOwnerAddr,
disableOwnerAddr,
sc,
changeWeightFlags.RPC,
)
}
func changeWeightACP99(
deployer *subnet.PublicDeployer,
network models.Network,
blockchainName string,
nodeID ids.NodeID,
weight uint64,
initiateTxHash 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)
}
}
ux.Logger.PrintToUser(logging.Yellow.Wrap("Validator manager owner %s pays for the initialization of the validator's weight change (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"), changeWeightFlags.RPC)
clusterName := sc.Networks[network.Name()].ClusterName
extraAggregatorPeers, err := blockchain.GetAggregatorExtraPeers(app, clusterName)
if err != nil {
return err
}
aggregatorLogger, err := signatureaggregator.NewSignatureAggregatorLogger(
changeWeightFlags.SigAggFlags.AggregatorLogLevel,
changeWeightFlags.SigAggFlags.AggregatorLogToStdout,
app.GetAggregatorLogDir(clusterName),
)
if err != nil {
return err
}
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()
signedMessage, validationID, rawTx, err := validatormanager.InitValidatorWeightChange(
aggregatorCtx,
ux.Logger.PrintToUser,
app,
network,
changeWeightFlags.RPC,
chainSpec,
externalValidatorManagerOwner,
validatorManagerOwner,
ownerPrivateKey,
nodeID,
aggregatorLogger,
validatorManagerAddress,
weight,
initiateTxHash,
signatureAggregatorEndpoint,
)
if err != nil {
return err
}
if rawTx != nil {
dump, err := evm.TxDump("Initializing Validator Weight Change", rawTx)
if err == nil {
ux.Logger.PrintToUser(dump)
}
return err
}
skipPChain := false
if newWeight != 0 {
// even if PChain already sent, validator should be available
validatorInfo, err := validator.GetValidatorInfo(network.SDKNetwork(), validationID)
if err != nil {
return err
}
if validatorInfo.Weight == newWeight {
ux.Logger.PrintToUser(logging.LightBlue.Wrap("The new Weight was already set on the P-Chain. Proceeding to the next step"))
skipPChain = true
}
}
if !skipPChain {
txID, _, err := deployer.SetL1ValidatorWeight(signedMessage)
if err != nil {
if newWeight != 0 || !strings.Contains(err.Error(), "could not load L1 validator: not found") {
return err
}
ux.Logger.PrintToUser(logging.LightBlue.Wrap("The Weight was already set to 0 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.FinishValidatorWeightChange(
aggregatorCtx,
app,
network,
changeWeightFlags.RPC,
chainSpec,
externalValidatorManagerOwner,
validatorManagerOwner,
ownerPrivateKey,
validationID,
aggregatorLogger,
validatorManagerAddress,
signedMessage,
newWeight,
signatureAggregatorEndpoint,
)
if err != nil {
return err
}
if rawTx != nil {
dump, err := evm.TxDump("Finish Validator Weight Change", rawTx)
if err == nil {
ux.Logger.PrintToUser(dump)
}
return err
}
ux.Logger.GreenCheckmarkToUser("Weight change successfully made")
return nil
}