Files

624 lines
19 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-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package upgradecmd
import (
"encoding/json"
"fmt"
"math"
"math/big"
"os"
"time"
"github.com/luxfi/cli/pkg/cobrautils"
cliprompts "github.com/luxfi/cli/pkg/prompts"
"github.com/luxfi/cli/pkg/utils"
"github.com/luxfi/cli/pkg/ux"
"github.com/luxfi/cli/pkg/vm"
"github.com/luxfi/constants"
"github.com/luxfi/evm/commontype"
"github.com/luxfi/evm/params"
"github.com/luxfi/evm/params/extras"
"github.com/luxfi/evm/precompile/contracts/deployerallowlist"
"github.com/luxfi/evm/precompile/contracts/feemanager"
"github.com/luxfi/evm/precompile/contracts/nativeminter"
"github.com/luxfi/evm/precompile/contracts/rewardmanager"
"github.com/luxfi/evm/precompile/contracts/txallowlist"
evmutils "github.com/luxfi/evm/utils"
"github.com/luxfi/geth/common"
luxlog "github.com/luxfi/log"
goethereummath "github.com/luxfi/math"
"github.com/luxfi/sdk/models"
"github.com/luxfi/sdk/prompts"
"github.com/spf13/cobra"
)
const (
feeConfigKey = "initialFeeConfig"
initialMintKey = "initialMint"
managerLabel = "manager"
adminLabel = "admin"
NativeMint = "Native Minting"
ContractAllowList = "Contract Deployment Allow List"
TxAllowList = "Transaction Allow List"
FeeManager = "Adjust Fee Settings Post Deploy"
RewardManager = "Customize Fees Distribution"
)
var generateConfirm bool
// lux blockchain upgrade generate
func newUpgradeGenerateCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "generate [blockchainName]",
Short: "Generate the configuration file to upgrade blockchain nodes",
Long: `The blockchain upgrade generate command builds a new upgrade.json file to customize your Blockchain.
It guides the user through the process using an interactive wizard.
IMPORTANT: This command requires interactive mode (TTY) due to the complexity of precompile
configuration. For non-interactive/CI environments, create the upgrade.json file manually
or use 'lux blockchain upgrade import' to import a pre-created configuration.
Use --yes/-y to skip the initial warning confirmation when running interactively.
Examples:
# Interactive mode (wizard)
lux blockchain upgrade generate mychain
# Skip initial warning
lux blockchain upgrade generate mychain --yes
# For CI/non-interactive: import a pre-created upgrade file instead
lux blockchain upgrade import mychain --upgrade-filepath ./upgrade.json`,
RunE: upgradeGenerateCmd,
Args: cobrautils.ExactArgs(1),
}
cmd.Flags().BoolVarP(&generateConfirm, "yes", "y", false, "Skip initial warning confirmation prompt")
return cmd
}
func upgradeGenerateCmd(_ *cobra.Command, args []string) error {
blockchainName := args[0]
if !app.GenesisExists(blockchainName) {
ux.Logger.PrintToUser("The provided blockchain name %q does not exist", blockchainName)
return nil
}
// This command requires interactive mode for the wizard
if !cliprompts.IsInteractive() {
return fmt.Errorf("this command requires interactive mode (TTY)\n"+
"For non-interactive/CI environments, create upgrade.json manually or use:\n"+
" lux blockchain upgrade import %s --upgrade-filepath ./upgrade.json", blockchainName)
}
// 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/lux-l1s/upgrade/customize-lux-l1#network-upgrades-enabledisable-precompiles ")+
luxlog.Reset.Wrap("for more information")))
if !generateConfirm {
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{
ContractAllowList,
FeeManager,
NativeMint,
TxAllowList,
RewardManager,
}
fmt.Println()
ux.Logger.PrintToUser("%s", luxlog.Yellow.Wrap(
"Luxgo 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 cancelled, err := promptParams(blockchainName, precomp, &precompiles.PrecompileUpgrades); err != nil {
return err
} else if cancelled {
continue
}
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
}
}
}
}
upgradeBytes, err := json.Marshal(&precompiles)
if err != nil {
return err
}
return writeUpgrade(blockchainName, upgradeBytes)
}
func writeUpgrade(blockchainName string, upgradeBytes []byte) error {
upgradeBytesFilePath := app.GetUpgradeBytesFilePath(blockchainName)
if utils.FileExists(upgradeBytesFilePath) {
timestamp := time.Now().UTC().Format("20060102150405")
renamedUpgradeBytesFilePath := upgradeBytesFilePath + "_" + timestamp
ux.Logger.PrintToUser("")
ux.Logger.PrintToUser("A previous upgrade is found. Renaming it to %s", renamedUpgradeBytesFilePath)
if err := os.Rename(upgradeBytesFilePath, renamedUpgradeBytesFilePath); err != nil {
return err
}
}
ux.Logger.PrintToUser("")
ux.Logger.PrintToUser("Writing upgrade into %s", upgradeBytesFilePath)
if err := app.WriteUpgradeFile(blockchainName, upgradeBytes); err != nil {
return err
}
PrintHowToApplyConfChangesMessage(blockchainName)
return nil
}
func PrintHowToApplyConfChangesMessage(blockchainName string) {
ux.Logger.PrintToUser("")
ux.Logger.PrintToUser("To apply the change on Local Network:")
ux.Logger.PrintToUser(" lux network stop")
ux.Logger.PrintToUser(" lux network start")
ux.Logger.PrintToUser("")
ux.Logger.PrintToUser("To apply the change to local validators on Testnet:")
ux.Logger.PrintToUser(" lux node local stop %s-local-node-testnet", blockchainName)
ux.Logger.PrintToUser(" lux node local start %s-local-start-testnet", blockchainName)
ux.Logger.PrintToUser("")
ux.Logger.PrintToUser("To apply the change to local validators on Mainnet:")
ux.Logger.PrintToUser(" lux node local stop %s-local-node-mainnet", blockchainName)
ux.Logger.PrintToUser(" lux node local start %s-local-start-mainnet", blockchainName)
}
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(blockchainName string, precomp string, precompiles *[]extras.PrecompileUpgrade) (bool, error) {
sc, err := app.LoadSidecar(blockchainName)
if err != nil {
return false, err
}
date, err := queryActivationTimestamp()
if err != nil {
return false, err
}
switch precomp {
case ContractAllowList:
return promptContractAllowListParams(&sc, precompiles, date)
case TxAllowList:
return promptTxAllowListParams(&sc, precompiles, date)
case NativeMint:
return promptNativeMintParams(&sc, precompiles, date)
case FeeManager:
return promptFeeManagerParams(&sc, precompiles, date)
case RewardManager:
return promptRewardManagerParams(&sc, precompiles, date)
default:
return false, fmt.Errorf("unexpected precompile identifier: %q", precomp)
}
}
func promptNativeMintParams(
sc *models.Sidecar,
precompiles *[]extras.PrecompileUpgrade,
date time.Time,
) (bool, error) {
initialMint := map[common.Address]*goethereummath.HexOrDecimal256{}
adminAddrs, managerAddrs, enabledAddrs, cancelled, err := promptAdminManagerAndEnabledAddresses(sc, "mint native tokens")
if cancelled || err != nil {
return cancelled, err
}
yes, err := app.Prompt.CaptureYesNo(fmt.Sprintf("Airdrop more tokens? (`%s` section in file)", initialMintKey))
if err != nil {
return false, err
}
if yes {
_, cancel, err := prompts.CaptureListDecision(
app.Prompt,
"How would you like to distribute your funds",
func(_ string) (string, error) {
addr, err := app.Prompt.CaptureAddress("Address to airdrop to")
if err != nil {
return "", err
}
amount, err := app.Prompt.CaptureUint64(fmt.Sprintf("Amount to airdrop (in %s units)", sc.TokenSymbol))
if err != nil {
return "", err
}
// Check for overflow when converting uint64 to int64
if amount > math.MaxInt64 {
return "", fmt.Errorf("amount %d exceeds maximum allowed value of %d", amount, math.MaxInt64)
}
// Convert crypto.Address to geth common.Address
gethAddr := common.BytesToAddress(addr.Bytes())
initialMint[gethAddr] = goethereummath.NewHexOrDecimal256(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: 0x9011E888251AB053B7bD1cdB598Db4f9DEd94714 (address) and 1000000000000000000 (value)",
)
if err != nil {
return false, err
}
if cancel {
return true, nil
}
}
config := nativeminter.NewConfig(
evmutils.NewUint64(uint64(date.Unix())), //nolint:gosec // G115: Unix time is positive
adminAddrs,
enabledAddrs,
managerAddrs,
initialMint,
)
upgrade := extras.PrecompileUpgrade{
Config: config,
}
*precompiles = append(*precompiles, upgrade)
return false, nil
}
func promptRewardManagerParams(
sc *models.Sidecar,
precompiles *[]extras.PrecompileUpgrade,
date time.Time,
) (bool, error) {
adminAddrs, managerAddrs, enabledAddrs, cancelled, err := promptAdminManagerAndEnabledAddresses(sc, "customize fee distribution")
if cancelled || err != nil {
return cancelled, err
}
initialConfig, err := ConfigureInitialRewardConfig()
if err != nil {
return false, err
}
config := rewardmanager.NewConfig(
evmutils.NewUint64(uint64(date.Unix())), //nolint:gosec // G115: Unix time is positive
adminAddrs,
enabledAddrs,
managerAddrs,
initialConfig,
)
upgrade := extras.PrecompileUpgrade{
Config: config,
}
*precompiles = append(*precompiles, upgrade)
return false, nil
}
func ConfigureInitialRewardConfig() (*rewardmanager.InitialRewardConfig, error) {
config := &rewardmanager.InitialRewardConfig{}
burnFees := "I am fine with transaction fees being burned
(Reward Manager Precompile OFF)"
distributeFees := "I want to customize the transaction fee distribution
(Reward Manager Precompile ON)"
options := []string{burnFees, distributeFees}
option, err := app.Prompt.CaptureList(
"By default, all transaction fees on Lux are burned (sent to a blackhole address). (Reward Manager Precompile)",
options,
)
if err != nil {
return config, err
}
if option == burnFees {
return config, nil
}
feeRcpdPrompt := "Allow block producers to claim fees?"
allowFeeRecipients, err := app.Prompt.CaptureYesNo(feeRcpdPrompt)
if err != nil {
return config, err
}
if allowFeeRecipients {
config.AllowFeeRecipients = true
return config, nil
}
rewardPrompt := "Provide the address to which fees will be sent to"
rewardAddress, err := app.Prompt.CaptureAddress(rewardPrompt)
if err != nil {
return config, err
}
// Convert crypto.Address to geth common.Address
config.RewardAddress = common.BytesToAddress(rewardAddress.Bytes())
return config, nil
}
func promptFeeManagerParams(
sc *models.Sidecar,
precompiles *[]extras.PrecompileUpgrade,
date time.Time,
) (bool, error) {
adminAddrs, managerAddrs, enabledAddrs, cancelled, err := promptAdminManagerAndEnabledAddresses(sc, "adjust the gas fees")
if cancelled || err != nil {
return cancelled, 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 false, err
}
var feeConfig *commontype.FeeConfig
if yes {
// Create default fee configuration
feeConfig = createFeeConfig(
big.NewInt(8000000), // gasLimit
2, // targetBlockRate
big.NewInt(25000000000), // minBaseFee
big.NewInt(15000000), // targetGas
big.NewInt(36), // baseFeeChangeDenominator
big.NewInt(0), // minBlockGasCost
big.NewInt(1000000), // maxBlockGasCost
big.NewInt(200000), // blockGasCostStep
)
}
config := feemanager.NewConfig(
evmutils.NewUint64(uint64(date.Unix())), //nolint:gosec // G115: Unix time is positive
adminAddrs,
enabledAddrs,
managerAddrs,
feeConfig,
)
upgrade := extras.PrecompileUpgrade{
Config: config,
}
*precompiles = append(*precompiles, upgrade)
return false, nil
}
func GetFeeConfig(config params.ChainConfig, useDefault bool) (
params.ChainConfig,
error,
) {
const (
lowOption = "Low block size / Low Throughput 12 mil gas per block"
mediumOption = "Medium block size / Medium Throughput 15 mil gas per block (C-Chain's setting)"
highOption = "High block size / High Throughput 20 mil gas per block"
customFee = "Customize fee config"
setGasLimit = "Set gas limit"
setBlockRate = "Set target block rate"
setMinBaseFee = "Set min base fee"
setTargetGas = "Set target gas"
setBaseFeeChangeDenominator = "Set base fee change denominator"
setMinBlockGas = "Set min block gas cost"
setMaxBlockGas = "Set max block gas cost"
setGasStep = "Set block gas cost step"
)
// Return default configuration if requested
if useDefault {
return config, nil
}
feeConfigOptions := []string{lowOption, mediumOption, highOption, customFee}
feeDefault, err := app.Prompt.CaptureList(
"How would you like to set fees",
feeConfigOptions,
)
if err != nil {
return config, err
}
// Dynamic fees configuration
useDynamicFees := false
if feeDefault != customFee {
useDynamicFees, err = app.Prompt.CaptureYesNo("Do you want to enable dynamic fees?")
if err != nil {
return config, err
}
}
switch feeDefault {
case lowOption:
extraConfig := params.GetExtra(&config)
setLowGasConfig(&extraConfig.FeeConfig, useDynamicFees)
params.SetExtra(&config, extraConfig)
return config, nil
case mediumOption:
extraConfig := params.GetExtra(&config)
setMediumGasConfig(&extraConfig.FeeConfig, useDynamicFees)
params.SetExtra(&config, extraConfig)
return config, nil
case highOption:
extraConfig := params.GetExtra(&config)
setHighGasConfig(&extraConfig.FeeConfig, useDynamicFees)
params.SetExtra(&config, extraConfig)
return config, nil
default:
ux.Logger.PrintToUser("Customizing fee config")
}
// Custom fee configuration
gasLimit, err := app.Prompt.CapturePositiveBigInt(setGasLimit)
if err != nil {
return config, err
}
blockRate, err := app.Prompt.CaptureUint64(setBlockRate)
if err != nil {
return config, err
}
minBaseFee, err := app.Prompt.CapturePositiveBigInt(setMinBaseFee)
if err != nil {
return config, err
}
targetGas, err := app.Prompt.CapturePositiveBigInt(setTargetGas)
if err != nil {
return config, err
}
baseFeeChangeDenominator, err := app.Prompt.CapturePositiveBigInt(setBaseFeeChangeDenominator)
if err != nil {
return config, err
}
minBlockGas, err := app.Prompt.CapturePositiveBigInt(setMinBlockGas)
if err != nil {
return config, err
}
maxBlockGas, err := app.Prompt.CapturePositiveBigInt(setMaxBlockGas)
if err != nil {
return config, err
}
gasStep, err := app.Prompt.CapturePositiveBigInt(setGasStep)
if err != nil {
return config, err
}
// Apply custom fee configuration
extraConfig := params.GetExtra(&config)
extraConfig.FeeConfig = commontype.FeeConfig{
GasLimit: gasLimit,
TargetBlockRate: blockRate,
MinBaseFee: minBaseFee,
TargetGas: targetGas,
BaseFeeChangeDenominator: baseFeeChangeDenominator,
MinBlockGasCost: minBlockGas,
MaxBlockGasCost: maxBlockGas,
BlockGasCostStep: gasStep,
}
params.SetExtra(&config, extraConfig)
return config, nil
}
func promptContractAllowListParams(
sc *models.Sidecar,
precompiles *[]extras.PrecompileUpgrade,
date time.Time,
) (bool, error) {
adminAddrs, managerAddrs, enabledAddrs, cancelled, err := promptAdminManagerAndEnabledAddresses(sc, "deploy smart contracts")
if cancelled || err != nil {
return cancelled, err
}
config := deployerallowlist.NewConfig(
evmutils.NewUint64(uint64(date.Unix())), //nolint:gosec // G115: Unix time is positive
adminAddrs,
enabledAddrs,
managerAddrs,
)
upgrade := extras.PrecompileUpgrade{
Config: config,
}
*precompiles = append(*precompiles, upgrade)
return false, nil
}
func promptTxAllowListParams(
sc *models.Sidecar,
precompiles *[]extras.PrecompileUpgrade,
date time.Time,
) (bool, error) {
adminAddrs, managerAddrs, enabledAddrs, cancelled, err := promptAdminManagerAndEnabledAddresses(sc, "issue transactions")
if cancelled || err != nil {
return cancelled, err
}
config := txallowlist.NewConfig(
evmutils.NewUint64(uint64(date.Unix())), //nolint:gosec // G115: Unix time is positive
adminAddrs,
enabledAddrs,
managerAddrs,
)
upgrade := extras.PrecompileUpgrade{
Config: config,
}
*precompiles = append(*precompiles, upgrade)
return false, nil
}
func promptAdminManagerAndEnabledAddresses(
sc *models.Sidecar,
action string,
) ([]common.Address, []common.Address, []common.Address, bool, error) {
allowList, cancelled, err := vm.GenerateAllowList(app, vm.AllowList{}, action, sc.VMVersion)
if cancelled || err != nil {
return nil, nil, nil, cancelled, err
}
return allowList.AdminAddresses, allowList.ManagerAddresses, allowList.EnabledAddresses, false, nil
}