Files
cli/cmd/nodecmd/create_devnet.go

323 lines
11 KiB
Go

// Copyright (C) 2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package nodecmd
import (
_ "embed"
"encoding/json"
"fmt"
"math/big"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"time"
"github.com/luxfi/cli/v2/pkg/ansible"
"github.com/luxfi/cli/v2/pkg/constants"
"github.com/luxfi/cli/v2/pkg/key"
"github.com/luxfi/cli/v2/pkg/models"
"github.com/luxfi/cli/v2/pkg/node"
"github.com/luxfi/cli/v2/pkg/ssh"
"github.com/luxfi/cli/v2/pkg/utils"
"github.com/luxfi/cli/v2/pkg/ux"
sdkutils "github.com/luxfi/cli/v2/sdk/utils"
"github.com/luxfi/node/v2/v2/config"
luxd_upgrade "github.com/luxfi/node/v2/v2/upgrade"
luxd_constants "github.com/luxfi/node/v2/v2/utils/constants"
"github.com/luxfi/crypto/bls/signer/localsigner"
"github.com/luxfi/node/v2/v2/utils/formatting"
"github.com/luxfi/node/v2/v2/utils/logging"
"github.com/luxfi/node/v2/v2/vms/platformvm/signer"
coreth_params "github.com/luxfi/geth/params"
"golang.org/x/exp/maps"
"golang.org/x/exp/slices"
)
// difference between unlock schedule locktime and startime in original genesis
const (
genesisLocktimeStartimeDelta = 2836800
hexa0Str = "0x0"
defaultLocalCChainFundedAddress = "8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC"
defaultLocalCChainFundedBalance = "0x295BE96E64066972000000"
allocationCommonEthAddress = "0xb3d82b1367d362de99ab59a658165aff520cbd4d"
)
//go:embed upgrade.json
var upgradeBytes []byte
func generateCustomCchainGenesis() ([]byte, error) {
chainConfig := &coreth_params.ChainConfig{
ChainID: coreth_params.LuxLocalChainID,
HomesteadBlock: big.NewInt(0),
DAOForkBlock: big.NewInt(0),
DAOForkSupport: true,
EIP150Block: big.NewInt(0),
EIP155Block: big.NewInt(0),
EIP158Block: big.NewInt(0),
ByzantiumBlock: big.NewInt(0),
ConstantinopleBlock: big.NewInt(0),
PetersburgBlock: big.NewInt(0),
IstanbulBlock: big.NewInt(0),
MuirGlacierBlock: big.NewInt(0),
NetworkUpgrades: coreth_params.GetNetworkUpgrades(luxd_upgrade.GetConfig(luxd_constants.LocalID)),
}
cChainGenesisMap := map[string]interface{}{}
cChainGenesisMap["config"] = chainConfig
cChainGenesisMap["nonce"] = hexa0Str
cChainGenesisMap["timestamp"] = hexa0Str
cChainGenesisMap["extraData"] = "0x00"
cChainGenesisMap["gasLimit"] = "0x5f5e100"
cChainGenesisMap["difficulty"] = hexa0Str
cChainGenesisMap["mixHash"] = "0x0000000000000000000000000000000000000000000000000000000000000000"
cChainGenesisMap["coinbase"] = "0x0000000000000000000000000000000000000000"
cChainGenesisMap["alloc"] = map[string]interface{}{
defaultLocalCChainFundedAddress: map[string]interface{}{
"balance": defaultLocalCChainFundedBalance,
},
}
cChainGenesisMap["number"] = hexa0Str
cChainGenesisMap["gasUsed"] = hexa0Str
cChainGenesisMap["parentHash"] = "0x0000000000000000000000000000000000000000000000000000000000000000"
return json.Marshal(cChainGenesisMap)
}
func generateCustomGenesis(
networkID uint32,
walletAddr string,
stakingAddr string,
hosts []*models.Host,
) ([]byte, error) {
genesisMap := map[string]interface{}{}
// cchain
cChainGenesisBytes, err := generateCustomCchainGenesis()
if err != nil {
return nil, err
}
genesisMap["cChainGenesis"] = string(cChainGenesisBytes)
// pchain genesis
genesisMap["networkID"] = networkID
startTime := time.Now().Unix()
genesisMap["startTime"] = startTime
initialStakers := []map[string]interface{}{}
for _, host := range hosts {
nodeDirPath := app.GetNodeInstanceDirPath(host.GetCloudID())
blsPath := filepath.Join(nodeDirPath, constants.BLSKeyFileName)
blsKey, err := os.ReadFile(blsPath)
if err != nil {
return nil, err
}
blsSk, err := localsigner.FromBytes(blsKey)
if err != nil {
return nil, err
}
p, err := signer.NewProofOfPossession(blsSk)
if err != nil {
return nil, err
}
pk, err := formatting.Encode(formatting.HexNC, p.PublicKey[:])
if err != nil {
return nil, err
}
pop, err := formatting.Encode(formatting.HexNC, p.ProofOfPossession[:])
if err != nil {
return nil, err
}
nodeID, err := getNodeID(nodeDirPath)
if err != nil {
return nil, err
}
initialStaker := map[string]interface{}{
"nodeID": nodeID,
"rewardAddress": walletAddr,
"delegationFee": 1000000,
"signer": map[string]interface{}{
"proofOfPossession": pop,
"publicKey": pk,
},
}
initialStakers = append(initialStakers, initialStaker)
}
genesisMap["initialStakeDuration"] = 31536000
genesisMap["initialStakeDurationOffset"] = 5400
genesisMap["initialStakers"] = initialStakers
lockTime := startTime + genesisLocktimeStartimeDelta
allocations := []interface{}{}
alloc := map[string]interface{}{
"luxAddr": walletAddr,
"ethAddr": allocationCommonEthAddress,
"initialAmount": 300000000000000000,
"unlockSchedule": []interface{}{
map[string]interface{}{"amount": 20000000000000000},
map[string]interface{}{"amount": 10000000000000000, "locktime": lockTime},
},
}
allocations = append(allocations, alloc)
alloc = map[string]interface{}{
"luxAddr": stakingAddr,
"ethAddr": allocationCommonEthAddress,
"initialAmount": 0,
"unlockSchedule": []interface{}{
map[string]interface{}{"amount": 10000000000000000, "locktime": lockTime},
},
}
allocations = append(allocations, alloc)
genesisMap["allocations"] = allocations
genesisMap["initialStakedFunds"] = []interface{}{
stakingAddr,
}
genesisMap["message"] = "{{ fun_quote }}"
return json.MarshalIndent(genesisMap, "", " ")
}
func setupDevnet(clusterName string, hosts []*models.Host, apiNodeIPMap map[string]string) error {
if err := node.CheckCluster(app, clusterName); err != nil {
return err
}
inventoryPath := app.GetAnsibleInventoryDirPath(clusterName)
ansibleHostIDs, err := ansible.GetAnsibleHostsFromInventory(inventoryPath)
if err != nil {
return err
}
ansibleHosts, err := ansible.GetHostMapfromAnsibleInventory(app.GetAnsibleInventoryDirPath(clusterName))
if err != nil {
return err
}
if err != nil {
return err
}
// set devnet network
endpointIP := ""
if len(apiNodeIPMap) > 0 {
endpointIP = maps.Values(apiNodeIPMap)[0]
} else {
endpointIP = ansibleHosts[ansibleHostIDs[0]].IP
}
endpoint := node.GetLuxdEndpoint(endpointIP)
network := models.NewDevnetNetwork(endpoint, 0)
network = models.NewNetworkFromCluster(network, clusterName)
// get random staking key for devnet genesis
k, err := key.NewSoft(network.ID)
if err != nil {
return err
}
stakingAddrStr := k.X()[0]
// get ewoq key as funded key for devnet genesis
k, err = key.LoadEwoq(network.ID)
if err != nil {
return err
}
walletAddrStr := k.X()[0]
// exclude API nodes from genesis file generation as they will have no stake
hostsAPI := utils.Filter(hosts, func(h *models.Host) bool {
return slices.Contains(maps.Keys(apiNodeIPMap), h.GetCloudID())
})
hostsWithoutAPI := utils.Filter(hosts, func(h *models.Host) bool {
return !slices.Contains(maps.Keys(apiNodeIPMap), h.GetCloudID())
})
hostsWithoutAPIIDs := sdkutils.Map(hostsWithoutAPI, func(h *models.Host) string { return h.NodeID })
// create genesis file at each node dir
genesisBytes, err := generateCustomGenesis(network.ID, walletAddrStr, stakingAddrStr, hostsWithoutAPI)
if err != nil {
return err
}
// make sure that custom genesis is saved to the subnet dir
if err := os.WriteFile(app.GetGenesisPath(blockchainName), genesisBytes, constants.WriteReadReadPerms); err != nil {
return err
}
// create luxd conf node.json at each node dir
bootstrapIPs := []string{}
bootstrapIDs := []string{}
// append makes sure that hostsWithoutAPI i.e. validators are proccessed first and API nodes will have full list of validators to bootstrap
for _, host := range append(hostsWithoutAPI, hostsAPI...) {
confMap := map[string]interface{}{}
confMap[config.HTTPHostKey] = ""
confMap[config.PublicIPKey] = host.IP
confMap[config.NetworkNameKey] = fmt.Sprintf("network-%d", network.ID)
confMap[config.BootstrapIDsKey] = strings.Join(bootstrapIDs, ",")
confMap[config.BootstrapIPsKey] = strings.Join(bootstrapIPs, ",")
confMap[config.GenesisFileKey] = filepath.Join(constants.DockerNodeConfigPath, constants.GenesisFileName)
confMap[config.UpgradeFileKey] = filepath.Join(constants.DockerNodeConfigPath, constants.UpgradeFileName)
confMap[config.ProposerVMUseCurrentHeightKey] = constants.DevnetFlagsProposerVMUseCurrentHeight
confBytes, err := json.MarshalIndent(confMap, "", " ")
if err != nil {
return err
}
if err := os.WriteFile(filepath.Join(app.GetNodeInstanceDirPath(host.GetCloudID()), constants.GenesisFileName), genesisBytes, constants.WriteReadReadPerms); err != nil {
return err
}
if err := os.WriteFile(filepath.Join(app.GetNodeInstanceDirPath(host.GetCloudID()), constants.UpgradeFileName), upgradeBytes, constants.WriteReadReadPerms); err != nil {
return err
}
if err := os.WriteFile(filepath.Join(app.GetNodeInstanceDirPath(host.GetCloudID()), constants.NodeFileName), confBytes, constants.WriteReadReadPerms); err != nil {
return err
}
if slices.Contains(hostsWithoutAPIIDs, host.NodeID) {
nodeID, err := getNodeID(app.GetNodeInstanceDirPath(host.GetCloudID()))
if err != nil {
return err
}
bootstrapIDs = append(bootstrapIDs, nodeID.String())
bootstrapIPs = append(bootstrapIPs, fmt.Sprintf("%s:9651", host.IP))
}
}
// update node/s genesis + conf and start
wg := sync.WaitGroup{}
wgResults := models.NodeResults{}
for _, host := range hosts {
wg.Add(1)
go func(nodeResults *models.NodeResults, host *models.Host) {
defer wg.Done()
keyPath := filepath.Join(app.GetNodesDir(), host.GetCloudID())
if err := ssh.RunSSHSetupDevNet(host, keyPath); err != nil {
nodeResults.AddResult(host.NodeID, nil, err)
ux.Logger.RedXToUser(utils.ScriptLog(host.NodeID, "Setup devnet err: %v", err))
return
}
ux.Logger.GreenCheckmarkToUser(utils.ScriptLog(host.NodeID, "Setup devnet"))
}(&wgResults, host)
}
wg.Wait()
ux.Logger.PrintLineSeparator()
for _, node := range hosts {
if wgResults.HasIDWithError(node.NodeID) {
ux.Logger.RedXToUser("Node %s is ERROR with error: %s", node.NodeID, wgResults.GetErrorHostMap()[node.NodeID])
} else {
nodeID, err := getNodeID(app.GetNodeInstanceDirPath(node.GetCloudID()))
if err != nil {
return err
}
ux.Logger.GreenCheckmarkToUser("Node %s[%s] is SETUP as devnet", node.GetCloudID(), nodeID)
}
}
// stop execution if at least one node failed
if wgResults.HasErrors() {
return fmt.Errorf("failed to deploy node(s) %s", wgResults.GetErrorHostMap())
}
ux.Logger.PrintLineSeparator()
ux.Logger.PrintToUser("Devnet Network Id: %s", logging.Green.Wrap(strconv.FormatUint(uint64(network.ID), 10)))
ux.Logger.PrintToUser("Devnet Endpoint: %s", logging.Green.Wrap(network.Endpoint))
ux.Logger.PrintLineSeparator()
// update cluster config with network information
clustersConfig, err := app.LoadClustersConfig()
if err != nil {
return err
}
clusterConfig := clustersConfig.Clusters[clusterName]
clusterConfig.Network = network
clustersConfig.Clusters[clusterName] = clusterConfig
return app.WriteClustersConfigFile(&clustersConfig)
}