mirror of
https://github.com/luxfi/cli.git
synced 2026-10-30 17:01:20 +00:00
496 lines
13 KiB
Go
496 lines
13 KiB
Go
// Copyright (C) 2022, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
package upgradecmd
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"math/big"
|
|
"time"
|
|
|
|
"github.com/luxfi/cli/v2/pkg/models"
|
|
"github.com/luxfi/node/v2/v2/utils/units"
|
|
gethmath "github.com/luxfi/geth/common/math"
|
|
"github.com/luxfi/geth/ethclient"
|
|
"go.uber.org/zap"
|
|
|
|
"github.com/luxfi/cli/v2/pkg/constants"
|
|
"github.com/luxfi/cli/v2/pkg/prompts"
|
|
"github.com/luxfi/cli/v2/pkg/ux"
|
|
"github.com/luxfi/cli/v2/pkg/vm"
|
|
luxlog "github.com/luxfi/log"
|
|
"github.com/luxfi/evm/v2/commontype"
|
|
"github.com/luxfi/evm/v2/params"
|
|
"github.com/luxfi/evm/v2/params/extras"
|
|
"github.com/luxfi/evm/v2/precompile/contracts/deployerallowlist"
|
|
"github.com/luxfi/evm/v2/precompile/contracts/feemanager"
|
|
"github.com/luxfi/evm/v2/precompile/contracts/nativeminter"
|
|
"github.com/luxfi/evm/v2/precompile/contracts/rewardmanager"
|
|
"github.com/luxfi/evm/v2/precompile/contracts/txallowlist"
|
|
"github.com/luxfi/geth/common"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
const (
|
|
blockTimestampKey = "blockTimestamp"
|
|
feeConfigKey = "initialFeeConfig"
|
|
initialMintKey = "initialMint"
|
|
adminAddressesKey = "adminAddresses"
|
|
enabledAddressesKey = "enabledAddresses"
|
|
|
|
enabledLabel = "enabled"
|
|
adminLabel = "admin"
|
|
)
|
|
|
|
var subnetName string
|
|
|
|
// lux subnet upgrade generate
|
|
func newUpgradeGenerateCmd() *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "generate [subnetName]",
|
|
Short: "Generate the configuration file to upgrade subnet nodes",
|
|
Long: `The subnet upgrade generate command builds a new upgrade.json file to customize your Subnet. It
|
|
guides the user through the process using an interactive wizard.`,
|
|
RunE: upgradeGenerateCmd,
|
|
Args: cobra.ExactArgs(1),
|
|
}
|
|
return cmd
|
|
}
|
|
|
|
func upgradeGenerateCmd(_ *cobra.Command, args []string) error {
|
|
subnetName = args[0]
|
|
if !app.GenesisExists(subnetName) {
|
|
ux.Logger.PrintToUser("The provided subnet name %q does not exist", subnetName)
|
|
return nil
|
|
}
|
|
// print some warning/info message
|
|
ux.Logger.PrintToUser("%s", luxlog.Bold.Wrap(luxlog.Yellow.Wrap(
|
|
"Performing a network upgrade requires coordinating the upgrade network-wide.")))
|
|
ux.Logger.PrintToUser("%s", luxlog.White.Wrap(luxlog.Reset.Wrap(
|
|
"A network upgrade changes the rule set used to process and verify blocks, " +
|
|
"such that any node that upgrades incorrectly or fails to upgrade by the time " +
|
|
"that upgrade goes into effect may become out of sync with the rest of the network.\n")))
|
|
ux.Logger.PrintToUser("%s", luxlog.Bold.Wrap(luxlog.Red.Wrap(
|
|
"Any mistakes in configuring network upgrades or coordinating them on validators " +
|
|
"may cause the network to halt and recovering may be difficult.")))
|
|
ux.Logger.PrintToUser("%s", luxlog.Reset.Wrap(
|
|
"Please consult " + luxlog.Cyan.Wrap(
|
|
"https://docs.lux.network/subnets/customize-a-subnet#network-upgrades-enabledisable-precompiles ") +
|
|
luxlog.Reset.Wrap("for more information")))
|
|
|
|
txt := "Press [Enter] to continue, or abort by choosing 'no'"
|
|
yes, err := app.Prompt.CaptureYesNo(txt)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !yes {
|
|
ux.Logger.PrintToUser("Aborted by user")
|
|
return nil
|
|
}
|
|
|
|
allPreComps := []string{
|
|
vm.ContractAllowList,
|
|
vm.FeeManager,
|
|
vm.NativeMint,
|
|
vm.TxAllowList,
|
|
vm.RewardManager,
|
|
}
|
|
|
|
fmt.Println()
|
|
ux.Logger.PrintToUser("%s", luxlog.Yellow.Wrap(
|
|
"Lux and this tool support configuring multiple precompiles. " +
|
|
"However, we suggest to only configure one per upgrade."))
|
|
fmt.Println()
|
|
|
|
// use the correct data types from evm right away
|
|
precompiles := extras.UpgradeConfig{
|
|
PrecompileUpgrades: make([]extras.PrecompileUpgrade, 0),
|
|
}
|
|
|
|
for {
|
|
precomp, err := app.Prompt.CaptureList("Select the precompile to configure", allPreComps)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ux.Logger.PrintToUser("Set parameters for the %q precompile", precomp)
|
|
if err := promptParams(precomp, &precompiles.PrecompileUpgrades); err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(allPreComps) > 1 {
|
|
yes, err := app.Prompt.CaptureNoYes("Should we configure another precompile?")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !yes {
|
|
break
|
|
}
|
|
|
|
for i := 0; i < len(allPreComps); i++ {
|
|
if allPreComps[i] == precomp {
|
|
allPreComps = append(allPreComps[:i], allPreComps[i+1:]...)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
jsonBytes, err := json.Marshal(&precompiles)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return app.WriteUpgradeFile(subnetName, jsonBytes)
|
|
}
|
|
|
|
func queryActivationTimestamp() (time.Time, error) {
|
|
const (
|
|
in5min = "In 5 minutes"
|
|
in1day = "In 1 day"
|
|
in1week = "In 1 week"
|
|
in2weeks = "In 2 weeks"
|
|
custom = "Custom"
|
|
)
|
|
options := []string{in5min, in1day, in1week, in2weeks, custom}
|
|
choice, err := app.Prompt.CaptureList("When should the precompile be activated?", options)
|
|
if err != nil {
|
|
return time.Time{}, err
|
|
}
|
|
|
|
var date time.Time
|
|
now := time.Now()
|
|
|
|
switch choice {
|
|
case in5min:
|
|
date = now.Add(5 * time.Minute)
|
|
case in1day:
|
|
date = now.Add(24 * time.Hour)
|
|
case in1week:
|
|
date = now.Add(7 * 24 * time.Hour)
|
|
case in2weeks:
|
|
date = now.Add(14 * 24 * time.Hour)
|
|
case custom:
|
|
date, err = app.Prompt.CaptureFutureDate(
|
|
"Enter the block activation UTC datetime in 'YYYY-MM-DD HH:MM:SS' format", time.Now().Add(time.Minute).UTC())
|
|
if err != nil {
|
|
return time.Time{}, err
|
|
}
|
|
}
|
|
|
|
ux.Logger.PrintToUser("The chosen block activation time is %s", date.Format(constants.TimeParseLayout))
|
|
return date, nil
|
|
}
|
|
|
|
func promptParams(precomp string, precompiles *[]extras.PrecompileUpgrade) error {
|
|
date, err := queryActivationTimestamp()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
switch precomp {
|
|
case vm.ContractAllowList:
|
|
return promptContractAllowListParams(precompiles, date)
|
|
case vm.TxAllowList:
|
|
return promptTxAllowListParams(precompiles, date)
|
|
case vm.NativeMint:
|
|
return promptNativeMintParams(precompiles, date)
|
|
case vm.FeeManager:
|
|
return promptFeeManagerParams(precompiles, date)
|
|
case vm.RewardManager:
|
|
return promptRewardManagerParams(precompiles, date)
|
|
default:
|
|
return fmt.Errorf("unexpected precompile identifier: %q", precomp)
|
|
}
|
|
}
|
|
|
|
func promptNativeMintParams(precompiles *[]extras.PrecompileUpgrade, date time.Time) error {
|
|
initialMint := map[common.Address]*gethmath.HexOrDecimal256{}
|
|
|
|
adminAddrs, enabledAddrs, err := promptAdminAndEnabledAddresses()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
yes, err := app.Prompt.CaptureYesNo(fmt.Sprintf("Airdrop more tokens? (`%s` section in file)", initialMintKey))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if yes {
|
|
_, cancel, err := prompts.CaptureListDecision(
|
|
app.Prompt,
|
|
"How would you like to distribute your funds",
|
|
func(s string) (string, error) {
|
|
addr, err := app.Prompt.CaptureAddress("Address to airdrop to")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
amount, err := app.Prompt.CaptureUint64("Amount to airdrop (in LUX units)")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
initialMint[addr] = (*gethmath.HexOrDecimal256)(big.NewInt(int64(amount)))
|
|
return fmt.Sprintf("%s-%d", addr.Hex(), amount), nil
|
|
},
|
|
"Add an address to amount pair",
|
|
"Address-Amount",
|
|
"Hex-formatted address and it's initial amount value, "+
|
|
"for example: 0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC (address) and 1000000000000000000 (value)",
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if cancel {
|
|
return errors.New("aborted by user")
|
|
}
|
|
}
|
|
|
|
timestamp := uint64(date.Unix())
|
|
config := nativeminter.NewConfig(
|
|
×tamp,
|
|
adminAddrs,
|
|
enabledAddrs,
|
|
nil, // managers addresses
|
|
initialMint,
|
|
)
|
|
upgrade := extras.PrecompileUpgrade{
|
|
Config: config,
|
|
}
|
|
*precompiles = append(*precompiles, upgrade)
|
|
return nil
|
|
}
|
|
|
|
func promptRewardManagerParams(precompiles *[]extras.PrecompileUpgrade, date time.Time) error {
|
|
adminAddrs, enabledAddrs, err := promptAdminAndEnabledAddresses()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
initialConfig, err := vm.ConfigureInitialRewardConfig(app)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
timestamp := uint64(date.Unix())
|
|
config := rewardmanager.NewConfig(
|
|
×tamp,
|
|
adminAddrs,
|
|
enabledAddrs,
|
|
nil, // managers addresses
|
|
initialConfig,
|
|
)
|
|
|
|
upgrade := extras.PrecompileUpgrade{
|
|
Config: config,
|
|
}
|
|
*precompiles = append(*precompiles, upgrade)
|
|
return nil
|
|
}
|
|
|
|
func promptFeeManagerParams(precompiles *[]extras.PrecompileUpgrade, date time.Time) error {
|
|
adminAddrs, enabledAddrs, err := promptAdminAndEnabledAddresses()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
yes, err := app.Prompt.CaptureYesNo(fmt.Sprintf(
|
|
"Do you want to update the fee config upon precompile activation? ('%s' section in file)", feeConfigKey))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var feeConfig *commontype.FeeConfig
|
|
|
|
if yes {
|
|
chainConfig, _, err := vm.GetFeeConfig(params.ChainConfig{}, app)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
feeConfig = &chainConfig.FeeConfig
|
|
}
|
|
|
|
timestamp := uint64(date.Unix())
|
|
config := feemanager.NewConfig(
|
|
×tamp,
|
|
adminAddrs,
|
|
enabledAddrs,
|
|
nil, // managers addresses
|
|
feeConfig,
|
|
)
|
|
upgrade := extras.PrecompileUpgrade{
|
|
Config: config,
|
|
}
|
|
*precompiles = append(*precompiles, upgrade)
|
|
return nil
|
|
}
|
|
|
|
func promptContractAllowListParams(precompiles *[]extras.PrecompileUpgrade, date time.Time) error {
|
|
adminAddrs, enabledAddrs, err := promptAdminAndEnabledAddresses()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
timestamp := uint64(date.Unix())
|
|
config := deployerallowlist.NewConfig(
|
|
×tamp,
|
|
adminAddrs,
|
|
enabledAddrs,
|
|
nil, // managers addresses
|
|
)
|
|
upgrade := extras.PrecompileUpgrade{
|
|
Config: config,
|
|
}
|
|
*precompiles = append(*precompiles, upgrade)
|
|
return nil
|
|
}
|
|
|
|
func promptTxAllowListParams(precompiles *[]extras.PrecompileUpgrade, date time.Time) error {
|
|
adminAddrs, enabledAddrs, err := promptAdminAndEnabledAddresses()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
timestamp := uint64(date.Unix())
|
|
config := txallowlist.NewConfig(
|
|
×tamp,
|
|
adminAddrs,
|
|
enabledAddrs,
|
|
nil, // managers addresses
|
|
)
|
|
upgrade := extras.PrecompileUpgrade{
|
|
Config: config,
|
|
}
|
|
*precompiles = append(*precompiles, upgrade)
|
|
return nil
|
|
}
|
|
|
|
func getCClient(apiEndpoint string, blockchainID string) (*ethclient.Client, error) {
|
|
cClient, err := ethclient.Dial(fmt.Sprintf("%s/ext/bc/%s/rpc", apiEndpoint, blockchainID))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return cClient, nil
|
|
}
|
|
|
|
func ensureAdminsHaveBalanceLocalNetwork(admins []common.Address, blockchainID string) error {
|
|
cClient, err := getCClient(constants.LocalAPIEndpoint, blockchainID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, admin := range admins {
|
|
// we can break at the first admin who has a non-zero balance
|
|
accountBalance, err := getAccountBalance(context.Background(), cClient, admin.String())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if accountBalance > float64(0) {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
return errors.New("at least one of the admin addresses requires a positive token balance")
|
|
}
|
|
|
|
func ensureAdminsHaveBalance(admins []common.Address, subnetName string) error {
|
|
if len(admins) < 1 {
|
|
return nil
|
|
}
|
|
|
|
if !app.GenesisExists(subnetName) {
|
|
ux.Logger.PrintToUser("The provided subnet name %q does not exist", subnetName)
|
|
return nil
|
|
}
|
|
|
|
// read in sidecar
|
|
sc, err := app.LoadSidecar(subnetName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
switch sc.VM {
|
|
case models.EVM:
|
|
// Currently only checking if admins have balance for subnets deployed in Local Network
|
|
if networkData, ok := sc.Networks["Local Network"]; ok {
|
|
blockchainID := networkData.BlockchainID.String()
|
|
err = ensureAdminsHaveBalanceLocalNetwork(admins, blockchainID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
default:
|
|
app.Log.Warn("Unsupported VM type", zap.Any("vm-type", sc.VM))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func getAccountBalance(ctx context.Context, cClient *ethclient.Client, addrStr string) (float64, error) {
|
|
addr := common.HexToAddress(addrStr)
|
|
ctx, cancel := context.WithTimeout(ctx, constants.RequestTimeout)
|
|
balance, err := cClient.BalanceAt(ctx, addr, nil)
|
|
defer cancel()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
// convert to nLux
|
|
balance = balance.Div(balance, big.NewInt(int64(units.Lux)))
|
|
if balance.Cmp(big.NewInt(0)) == 0 {
|
|
return 0, nil
|
|
}
|
|
return float64(balance.Uint64()) / float64(units.Lux), nil
|
|
}
|
|
|
|
func promptAdminAndEnabledAddresses() ([]common.Address, []common.Address, error) {
|
|
var admin, enabled []common.Address
|
|
|
|
for {
|
|
if err := captureAddress(adminLabel, &admin); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
if err := ensureAdminsHaveBalance(admin, subnetName); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
if err := captureAddress(enabledLabel, &enabled); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
if len(enabled) == 0 && len(admin) == 0 {
|
|
ux.Logger.PrintToUser(
|
|
"We need at least one address for either '%s' or '%s'. Otherwise abort.", enabledAddressesKey, adminAddressesKey)
|
|
continue
|
|
}
|
|
return admin, enabled, nil
|
|
}
|
|
}
|
|
|
|
func captureAddress(which string, addrsField *[]common.Address) error {
|
|
yes, err := app.Prompt.CaptureYesNo(fmt.Sprintf("Add '%sAddresses'?", which))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if yes {
|
|
var (
|
|
cancel bool
|
|
err error
|
|
)
|
|
*addrsField, cancel, err = prompts.CaptureListDecision(
|
|
app.Prompt,
|
|
fmt.Sprintf("Provide '%sAddresses'", which),
|
|
app.Prompt.CaptureAddress,
|
|
"Add an address",
|
|
"Address",
|
|
fmt.Sprintf("Hex-formatted %s addresses", which),
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if cancel {
|
|
return errors.New("aborted by user")
|
|
}
|
|
}
|
|
return nil
|
|
}
|