Files

688 lines
22 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Copyright (C) 2022, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package subnetcmd
import (
"context"
"errors"
"fmt"
"math"
"os"
"strings"
"time"
"github.com/luxfi/cli/v2/pkg/txutils"
"github.com/luxfi/cli/v2/pkg/prompts"
"github.com/luxfi/node/v2/v2/vms/components/verify"
"github.com/luxfi/node/v2/v2/vms/platformvm"
"github.com/luxfi/cli/v2/pkg/constants"
"github.com/luxfi/cli/v2/pkg/ux"
"github.com/luxfi/node/v2/v2/genesis"
es "github.com/luxfi/cli/v2/pkg/elasticsubnet"
"github.com/luxfi/cli/v2/pkg/models"
subnet "github.com/luxfi/cli/v2/pkg/subnet"
"github.com/luxfi/ids"
"github.com/luxfi/node/v2/v2/vms/secp256k1fx"
"github.com/olekukonko/tablewriter"
"github.com/spf13/cobra"
)
const (
localDeployment = "Existing local deployment"
testnetDeployment = "Testnet"
mainnetDeployment = "Mainnet (coming soon)"
subnetIsElasticError = "subnet is already elastic"
)
var (
transformLocal bool
tokenNameFlag string
tokenSymbolFlag string
useDefaultConfig bool
overrideWarning bool
transformValidators bool
denominationFlag int
)
// lux subnet elastic
func newElasticCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "elastic [subnetName]",
Short: "Transforms a subnet into elastic subnet",
Long: `The elastic command enables anyone to be a validator of a Subnet by simply staking its token on the
P-Chain. When enabling Elastic Validation, the creator permanently locks the Subnet from future modification
(they relinquish their control keys), specifies an Lux Native Token (ANT) that validators must use for staking
and that will be distributed as staking rewards, and provides a set of parameters that govern how the Subnet’s staking
mechanics will work.`,
SilenceUsage: true,
Args: cobra.ExactArgs(1),
RunE: transformElasticSubnet,
}
cmd.Flags().BoolVarP(&transformLocal, "local", "l", false, "transform a subnet on a local network")
cmd.Flags().BoolVar(&deployTestnet, "testnet", false, "remove from `testnet` deployment (alias for `testnet`)")
cmd.Flags().StringVar(&tokenNameFlag, "tokenName", "", "specify the token name")
cmd.Flags().StringVar(&tokenSymbolFlag, "tokenSymbol", "", "specify the token symbol")
cmd.Flags().BoolVar(&useDefaultConfig, "default", false, "use default elastic subnet config values")
cmd.Flags().BoolVar(&overrideWarning, "force", false, "override transform into elastic subnet warning")
cmd.Flags().Uint64Var(&stakeAmount, "stake-amount", 0, "amount of tokens to stake on validator")
cmd.Flags().StringVar(&startTimeStr, "start-time", "", "start time that validator starts validating")
cmd.Flags().DurationVar(&duration, "staking-period", 0, "how long validator validates for after start time")
cmd.Flags().BoolVar(&transformValidators, "transform-validators", false, "transform validators to permissionless validators")
cmd.Flags().IntVar(&denominationFlag, "denomination", -1, "specify the token denomination")
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().StringVarP(&keyName, "key", "k", "", "select the key to use [testnet only]")
cmd.Flags().StringSliceVar(&subnetAuthKeys, "subnet-auth-keys", nil, "control keys that will be used to authenticate the transformSubnet tx")
cmd.Flags().StringVar(&outputTxPath, "output-tx-path", "", "file path of the transformSubnet tx")
return cmd
}
func checkIfSubnetIsElasticOnLocal(sc models.Sidecar) bool {
if _, ok := sc.ElasticSubnet[models.Local.String()]; ok {
return true
}
return false
}
func createAssetID(deployer *subnet.PublicDeployer,
maxSupply uint64,
subnetID ids.ID,
tokenName string,
tokenSymbol string,
tokenDenomination int,
recipientAddr ids.ShortID,
) (ids.ID, error) {
if tokenDenomination > math.MaxUint8 {
return ids.Empty, errors.New("token denomination cannot exceed 32")
}
owner := &secp256k1fx.OutputOwners{
Threshold: 1,
Addrs: []ids.ShortID{
recipientAddr,
},
}
initialState := map[uint32][]verify.State{
0: {
&secp256k1fx.TransferOutput{
Amt: maxSupply,
OutputOwners: *owner,
},
},
}
return deployer.CreateAssetTx(subnetID, tokenName, tokenSymbol, byte(tokenDenomination), initialState)
}
func exportToPChain(deployer *subnet.PublicDeployer,
subnetID ids.ID,
subnetAssetID ids.ID,
recipientAddr ids.ShortID,
maxSupply uint64,
) (ids.ID, error) {
owner := &secp256k1fx.OutputOwners{
Threshold: 1,
Addrs: []ids.ShortID{
recipientAddr,
},
}
return deployer.ExportToPChainTx(subnetID, subnetAssetID, owner, maxSupply)
}
func importFromXChain(deployer *subnet.PublicDeployer,
subnetID ids.ID,
recipientAddr ids.ShortID,
) (ids.ID, error) {
owner := &secp256k1fx.OutputOwners{
Threshold: 1,
Addrs: []ids.ShortID{
recipientAddr,
},
}
return deployer.ImportFromXChain(subnetID, owner)
}
func transformElasticSubnet(_ *cobra.Command, args []string) error {
subnetName := args[0]
if !app.SubnetConfigExists(subnetName) {
return errors.New("subnet does not exist")
}
sc, err := app.LoadSidecar(subnetName)
if err != nil {
return fmt.Errorf("unable to load sidecar: %w", err)
}
var network models.Network
switch {
case deployTestnet:
network = models.Testnet
case deployMainnet:
network = models.Mainnet
case transformLocal:
network = models.Local
}
if network == models.Undefined {
networkToUpgrade, err := selectNetworkToTransform(sc)
if err != nil {
return err
}
switch networkToUpgrade {
case localDeployment:
network = models.Local
case testnetDeployment:
network = models.Testnet
default:
return errors.New("elastic subnet transformation is not yet supported on Mainnet")
}
}
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
}
subnetID := sc.Networks[network.String()].SubnetID
if os.Getenv(constants.SimulatePublicNetwork) != "" {
subnetID = sc.Networks[models.Local.String()].SubnetID
}
if subnetID == ids.Empty {
return errNoSubnetID
}
if network != models.Local {
isAlreadyElastic, err := CheckSubnetIsElastic(subnetID, network)
if err != nil && err.Error() != subnetIsElasticError {
return err
}
if isAlreadyElastic {
return errors.New(subnetIsElasticError)
}
}
tokenName := ""
if tokenNameFlag == "" {
tokenName, err = getTokenName()
if err != nil {
return err
}
} else {
tokenName = tokenNameFlag
}
tokenSymbol := ""
if tokenSymbolFlag == "" {
tokenSymbol, err = getTokenSymbol()
if err != nil {
return err
}
} else {
tokenSymbol = tokenSymbolFlag
}
tokenDenomination := 0
if network != models.Local {
if denominationFlag == -1 {
tokenDenomination, err = getTokenDenomination()
if err != nil {
return err
}
} else {
tokenDenomination = denominationFlag
}
}
elasticSubnetConfig, err := es.GetElasticSubnetConfig(app, tokenSymbol, useDefaultConfig)
if err != nil {
return err
}
elasticSubnetConfig.SubnetID = subnetID
switch network {
case models.Local:
return transformElasticSubnetLocal(sc, subnetName, tokenName, tokenSymbol, elasticSubnetConfig)
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
}
// get keychain accessor
kc, err := GetKeychain(useLedger, ledgerAddresses, keyName, network)
if err != nil {
return err
}
recipientAddr := kc.Addresses().List()[0]
deployer := subnet.NewPublicDeployer(app, useLedger, kc, network)
txHasOccurred, txID := checkIfTxHasOccurred(&sc, network, "CreateAssetTx")
var assetID ids.ID
// TODO: replace sleep functions with sticky API sessions
if txHasOccurred {
ux.Logger.PrintToUser("Skipping CreateAssetTx, transforming subnet with asset ID %s...", txID.String())
assetID = txID
} else {
assetID, err = createAssetID(deployer, elasticSubnetConfig.MaxSupply, subnetID, tokenName, tokenSymbol, tokenDenomination, recipientAddr)
if err != nil {
return err
}
err = app.UpdateSidecarElasticSubnetPartialTx(&sc, network, "CreateAssetTx", assetID)
if err != nil {
return err
}
// we need to sleep after each operation to make sure that UTXO is available for consumption
time.Sleep(5 * time.Second)
}
txHasOccurred, _ = checkIfTxHasOccurred(&sc, network, "ExportTx")
if !txHasOccurred {
txID, err = exportToPChain(deployer, subnetID, assetID, recipientAddr, elasticSubnetConfig.MaxSupply)
if err != nil {
return err
}
err = app.UpdateSidecarElasticSubnetPartialTx(&sc, network, "ExportTx", txID)
if err != nil {
return err
}
time.Sleep(5 * time.Second)
} else {
ux.Logger.PrintToUser("Skipping ExportTx...")
}
txHasOccurred, _ = checkIfTxHasOccurred(&sc, network, "ImportTx")
if !txHasOccurred {
txID, err = importFromXChain(deployer, subnetID, recipientAddr)
if err != nil {
return err
}
err = app.UpdateSidecarElasticSubnetPartialTx(&sc, network, "ImportTx", txID)
if err != nil {
return err
}
time.Sleep(5 * time.Second)
} else {
ux.Logger.PrintToUser("Skipping ImportTx...")
}
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 issue transform subnet tx: %s", subnetAuthKeys)
isFullySigned, txID, tx, remainingSubnetAuthKeys, err := deployer.TransformSubnetTx(controlKeys, subnetAuthKeys, elasticSubnetConfig, subnetID, assetID)
if err != nil {
return err
}
if !isFullySigned {
if err := SaveNotFullySignedTx(
"Transform Subnet",
tx,
subnetName,
subnetAuthKeys,
remainingSubnetAuthKeys,
outputTxPath,
false,
); err != nil {
return err
}
} else {
elasticSubnetConfig.AssetID = assetID
if err = app.CreateElasticSubnetConfig(subnetName, &elasticSubnetConfig); err != nil {
return err
}
if err = app.UpdateSidecarElasticSubnet(&sc, network, subnetID, assetID, txID, tokenName, tokenSymbol); err != nil {
return fmt.Errorf("elastic subnet transformation was successful, but failed to update sidecar: %w", err)
}
PrintTransformResults(subnetName, txID, subnetID, tokenName, tokenSymbol, assetID)
}
return nil
}
func transformElasticSubnetLocal(sc models.Sidecar, subnetName string, tokenName string, tokenSymbol string, elasticSubnetConfig models.ElasticSubnetConfig) error {
if checkIfSubnetIsElasticOnLocal(sc) {
return fmt.Errorf("%s is already an elastic subnet", subnetName)
}
var err error
subnetID := sc.Networks[models.Local.String()].SubnetID
if subnetID == ids.Empty {
return errNoSubnetID
}
if !overrideWarning {
yes, err := app.Prompt.CaptureNoYes("WARNING: Transforming a Permissioned Subnet into an Elastic Subnet is an irreversible operation. Continue?")
if err != nil {
return err
}
if !yes {
return nil
}
}
ux.Logger.PrintToUser("Starting Elastic Subnet Transformation")
cancel := make(chan struct{})
go ux.PrintWait(cancel)
testKey := genesis.EWOQKey
keyChain := secp256k1fx.NewKeychain(testKey)
txID, assetID, err := subnet.IssueTransformSubnetTx(elasticSubnetConfig, keyChain, subnetID, tokenName, tokenSymbol, elasticSubnetConfig.MaxSupply)
close(cancel)
if err != nil {
return err
}
ux.Logger.PrintToUser("")
ux.Logger.PrintToUser("Subnet Successfully Transformed To Elastic Subnet!")
elasticSubnetConfig.AssetID = assetID
if err = app.CreateElasticSubnetConfig(subnetName, &elasticSubnetConfig); err != nil {
return err
}
if err = app.UpdateSidecarElasticSubnet(&sc, models.Local, subnetID, assetID, txID, tokenName, tokenSymbol); err != nil {
return fmt.Errorf("elastic subnet transformation was successful, but failed to update sidecar: %w", err)
}
if !transformValidators {
if !overrideWarning {
yes, err := app.Prompt.CaptureNoYes("Do you want to transform existing validators to permissionless validators with equal weight? " +
"Press <No> if you want to customize the structure of your permissionless validators")
if err != nil {
return err
}
if !yes {
return nil
}
ux.Logger.PrintToUser("Transforming validators to permissionless validators")
if err = transformValidatorsToPermissionlessLocal(sc, subnetID, subnetName); err != nil {
return err
}
}
} else {
ux.Logger.PrintToUser("Transforming validators to permissionless validators")
if err = transformValidatorsToPermissionlessLocal(sc, subnetID, subnetName); err != nil {
return err
}
}
PrintTransformResults(subnetName, txID, subnetID, tokenName, tokenSymbol, assetID)
return nil
}
// select which network to transform to elastic subnet
func promptNetworkElastic(sc models.Sidecar, prompt string) (string, error) {
var networkOptions []string
for network := range sc.Networks {
switch network {
case models.Local.String():
networkOptions = append(networkOptions, localDeployment)
case models.Testnet.String():
networkOptions = append(networkOptions, testnetDeployment)
case models.Mainnet.String():
networkOptions = append(networkOptions, mainnetDeployment)
}
}
if len(networkOptions) == 0 {
return "", errors.New("no deployment target available, please first deploy created subnet")
}
selectedDeployment, err := app.Prompt.CaptureList(prompt, networkOptions)
if err != nil {
return "", err
}
return selectedDeployment, nil
}
// select which network to transform to elastic subnet
func selectNetworkToTransform(sc models.Sidecar) (string, error) {
var networkOptions []string
networkPrompt := "Which network should transform into an elastic Subnet?"
for network := range sc.Networks {
switch network {
case models.Local.String():
networkOptions = append(networkOptions, localDeployment)
case models.Testnet.String():
networkOptions = append(networkOptions, testnetDeployment)
case models.Mainnet.String():
networkOptions = append(networkOptions, mainnetDeployment)
}
}
if len(networkOptions) == 0 {
return "", errors.New("no deployment target available, please first deploy created subnet")
}
selectedDeployment, err := app.Prompt.CaptureList(networkPrompt, networkOptions)
if err != nil {
return "", err
}
return selectedDeployment, nil
}
func PrintTransformResults(chain string, txID ids.ID, subnetID ids.ID, tokenName string, tokenSymbol string, assetID ids.ID) {
const art = "\n ______ _ _ _ _____ _ _ _______ __ _____ __ _ " +
"\n | ____| | | | (_) / ____| | | | | |__ __| / _| / ____| / _| | |" +
"\n | |__ | | __ _ ___| |_ _ ___ | (___ _ _| |__ _ __ ___| |_ | |_ __ __ _ _ __ ___| |_ ___ _ __ _ __ ___ | (___ _ _ ___ ___ ___ ___ ___| |_ _ _| |" +
"\n | __| | |/ _` / __| __| |/ __| \\___ \\| | | | '_ \\| '_ \\ / _ \\ __| | | '__/ _` | '_ \\/ __| _/ _ \\| '__| '_ ` _ \\ \\___ \\| | | |/ __/ __/ _ \\/ __/ __| _| | | | |" +
"\n | |____| | (_| \\__ \\ |_| | (__ ____) | |_| | |_) | | | | __/ |_ | | | | (_| | | | \\__ \\ || (_) | | | | | | | | ____) | |_| | (_| (_| __/\\__ \\__ \\ | | |_| | |" +
"\n |______|_|\\__,_|___/\\__|_|\\___| |_____/ \\__,_|_.__/|_| |_|\\___|\\__| |_|_| \\__,_|_| |_|___/_| \\___/|_| |_| |_| |_| |_____/ \\__,_|\\___\\___\\___||___/___/_| \\__,_|_|" +
"\n"
fmt.Print(art)
table := tablewriter.NewWriter(os.Stdout)
table.SetRowLine(true)
table.SetAutoMergeCells(true)
table.Append([]string{"Token Name", tokenName})
table.Append([]string{"Token Symbol", tokenSymbol})
table.Append([]string{"Asset ID", assetID.String()})
table.Append([]string{"Chain Name", chain})
table.Append([]string{"Subnet ID", subnetID.String()})
table.Append([]string{"P-Chain TXID", txID.String()})
table.Render()
}
func getTokenName() (string, error) {
ux.Logger.PrintToUser("Select a name for your subnet's native token")
tokenName, err := app.Prompt.CaptureString("Token name")
if err != nil {
return "", err
}
return tokenName, nil
}
func getTokenSymbol() (string, error) {
ux.Logger.PrintToUser("Select a symbol for your subnet's native token")
tokenSymbol, err := app.Prompt.CaptureString("Token symbol")
if err != nil {
return "", err
}
return tokenSymbol, nil
}
func checkAllLocalNodesAreCurrentValidators(subnetID ids.ID) error {
api := constants.LocalAPIEndpoint
pClient := platformvm.NewClient(api)
ctx := context.Background()
validators, err := pClient.GetCurrentValidators(ctx, subnetID, nil)
if err != nil {
return err
}
for _, localVal := range defaultLocalNetworkNodeIDs {
currentValidator := false
for _, validator := range validators {
if validator.NodeID.String() == localVal {
currentValidator = true
}
}
if !currentValidator {
return fmt.Errorf("%s is still not a current validator of the elastic subnet", localVal)
}
}
return nil
}
func transformValidatorsToPermissionlessLocal(sc models.Sidecar, subnetID ids.ID, subnetName string) error {
stakedTokenAmount, err := promptStakeAmount(subnetName)
if err != nil {
return err
}
validators, err := subnet.GetSubnetValidators(subnetID)
if err != nil {
return err
}
validatorList := make([]ids.NodeID, len(validators))
for i, v := range validators {
validatorList[i] = v.NodeID
}
numToRemoveInitially := len(validatorList) - 1
for _, validator := range validatorList {
// Remove first 4 nodes locally, wait for minimum lead time (25 seconds) and then remove the last node
// so that we don't end up with a subnet without any current validators
if numToRemoveInitially > 0 {
err = handleRemoveAndAddValidators(sc, subnetID, validator, stakedTokenAmount)
if err != nil {
return err
}
numToRemoveInitially -= 1
} else {
ux.Logger.PrintToUser("Waiting for the first four nodes to be activated as permissionless validators...")
time.Sleep(constants.StakingMinimumLeadTime)
err = handleRemoveAndAddValidators(sc, subnetID, validator, stakedTokenAmount)
if err != nil {
return err
}
}
}
time.Sleep(constants.StakingMinimumLeadTime)
return checkAllLocalNodesAreCurrentValidators(subnetID)
}
func handleRemoveAndAddValidators(sc models.Sidecar, subnetID ids.ID, validator ids.NodeID, stakedAmount uint64) error {
startTime := time.Now().Add(constants.StakingMinimumLeadTime).UTC()
endTime := startTime.Add(constants.MinStakeDuration)
testKey := genesis.EWOQKey
keyChain := secp256k1fx.NewKeychain(testKey)
_, err := subnet.IssueRemoveSubnetValidatorTx(keyChain, subnetID, validator)
if err != nil {
return err
}
ux.Logger.PrintToUser("Validator %s removed", validator.String())
assetID := sc.ElasticSubnet[models.Local.String()].AssetID
txID, err := subnet.IssueAddPermissionlessValidatorTx(keyChain, subnetID, validator, stakedAmount, assetID, uint64(startTime.Unix()), uint64(endTime.Unix()))
if err != nil {
return err
}
ux.Logger.PrintToUser("%s successfully joined elastic subnet as permissionless validator!", validator.String())
if err = app.UpdateSidecarPermissionlessValidator(&sc, models.Local, validator.String(), txID); err != nil {
return fmt.Errorf("joining permissionless subnet was successful, but failed to update sidecar: %w", err)
}
return nil
}
func getTokenDenomination() (int, error) {
ux.Logger.PrintToUser("What's the denomination for your token?")
ux.Logger.PrintToUser("Denomination determines how balances of this asset are displayed by user interfaces. " +
"If denomination is 0, 100 units of this asset are displayed as 100. If denomination is 1, 100 units of this asset are displayed as 10.0.")
tokenDenomination, err := app.Prompt.CapturePositiveInt(
"Token Denomination",
[]prompts.Comparator{
{
Label: "Min Denomination Value",
Type: prompts.MoreThanEq,
Value: 0,
},
{
Label: "Max Denomination Value",
Type: prompts.LessThanEq,
Value: 32,
},
},
)
if err != nil {
return 0, err
}
return tokenDenomination, nil
}
func CheckSubnetIsElastic(subnetID ids.ID, network models.Network) (bool, error) {
var apiURL string
switch network {
case models.Mainnet:
apiURL = constants.MainnetAPIEndpoint
case models.Testnet:
apiURL = constants.TestnetAPIEndpoint
default:
return false, fmt.Errorf("invalid network: %s", network)
}
pClient := platformvm.NewClient(apiURL)
ctx, cancel := context.WithTimeout(context.Background(), constants.E2ERequestTimeout)
defer cancel()
_, _, err := pClient.GetCurrentSupply(ctx, subnetID)
if err != nil {
// if subnet is already elastic it will return "not found" error
if strings.Contains(err.Error(), "not found") {
return false, errors.New(subnetIsElasticError)
}
return false, err
}
return true, nil
}
func checkIfTxHasOccurred(
sc *models.Sidecar,
network models.Network,
txName string,
) (bool, ids.ID) {
if sc.ElasticSubnet == nil {
return false, ids.Empty
}
if sc.ElasticSubnet[network.String()].Txs != nil {
txID, ok := sc.ElasticSubnet[network.String()].Txs[txName]
if ok {
return true, txID
}
}
return false, ids.Empty
}