Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Devnet #132

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions avalanche/network.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ const (
Mainnet
Fuji
Devnet
DevnetAPIEndpoint = ""
DevnetNetworkID = 1338
)

const (
Expand Down Expand Up @@ -82,6 +84,16 @@ func MainnetNetwork() Network {
return NewNetwork(Mainnet, constants.MainnetID, MainnetAPIEndpoint)
}

func DevnetNetwork(endpoint string, id uint32) Network {
if endpoint == "" {
endpoint = DevnetAPIEndpoint
}
if id == 0 {
id = DevnetNetworkID
}
return NewNetwork(Devnet, id, endpoint)
}

func (n Network) GenesisParams() *genesis.Params {
switch n.Kind {
case Devnet:
Expand Down
4 changes: 4 additions & 0 deletions constants/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,18 @@ const (
SSHFileOpsTimeout = 100 * time.Second
SSHPOSTTimeout = 10 * time.Second
SSHScriptTimeout = 2 * time.Minute
SSHDirOpsTimeout = 10 * time.Second
RemoteHostUser = "ubuntu"
DockerNodeConfigPath = "/.avalanchego/configs/"

// node
CloudNodeCLIConfigBasePath = "/home/ubuntu/.avalanche-cli/"
CloudNodeStakingPath = "/home/ubuntu/.avalanchego/staking/"
CloudNodeConfigPath = "/home/ubuntu/.avalanchego/configs/"
ServicesDir = "services"
DashboardsDir = "dashboards"
GenesisFileName = "genesis.json"
NodeFileName = "node.json"
// services
ServiceAvalanchego = "avalanchego"
ServicePromtail = "promtail"
Expand Down
322 changes: 322 additions & 0 deletions node/devnet.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,322 @@
// Copyright (C) 2024, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.

package node

import (
"encoding/json"
"fmt"
"path/filepath"
"slices"
"strconv"
"strings"
"sync"
"time"

"github.com/ava-labs/avalanche-tooling-sdk-go/avalanche"
"github.com/ava-labs/avalanche-tooling-sdk-go/key"
"github.com/ava-labs/avalanchego/config"
"github.com/ava-labs/avalanchego/utils/crypto/bls"
"github.com/ava-labs/avalanchego/utils/formatting"
"github.com/ava-labs/avalanchego/utils/logging"
"github.com/ava-labs/avalanchego/vms/platformvm/signer"
"golang.org/x/exp/maps"

"github.com/ava-labs/avalanche-tooling-sdk-go/constants"
"github.com/ava-labs/avalanche-tooling-sdk-go/utils"
coreth_params "github.com/ava-labs/coreth/params"
)

// difference between unlock schedule locktime and startime in original genesis
const (
genesisLocktimeStartimeDelta = 2836800
hexa0Str = "0x0"
defaultLocalCChainFundedAddress = "8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC"
defaultLocalCChainFundedBalance = "0x295BE96E64066972000000"
allocationCommonEthAddress = "0xb3d82b1367d362de99ab59a658165aff520cbd4d"
)

// func SetupDevnet(clusterName string, hosts []*models.Host, apiNodeIPMap map[string]string) error {
// func SetupDevnet(clusterName string, hosts []*Node, apiNodeIPMap map[string]string) error {
func SetupDevnet(hosts []*Node, apiNodeIPMap map[string]string) error {
//ansibleHosts, err := ansible.GetHostMapfromAnsibleInventory(app.GetAnsibleInventoryDirPath(clusterName))
//if err != nil {
// return err
//}

// set devnet network
endpointIP := ""
if len(apiNodeIPMap) > 0 {
endpointIP = maps.Values(apiNodeIPMap)[0]
} else {
//endpointIP = ansibleHosts[ansibleHostIDs[0]].IP
endpointIP = hosts[0].IP
}
endpoint := fmt.Sprintf("http://%s:%d", endpointIP, constants.AvalanchegoAPIPort)
network := avalanche.DevnetNetwork(endpoint, 0)
// network = models.NewNetworkFromCluster(network, clusterName)

// get random staking key for devnet genesis
k, err := key.NewSoft()
if err != nil {
return err
}
// stakingAddrStr := k.X()[0]
stakingAddrStr, err := k.X(network.HRP())
if err != nil {
return err
}

// get ewoq key as funded key for devnet genesis
// k, err = key.LoadEwoq(network.ID)
k, err = key.LoadEwoq()
if err != nil {
return err
}
// walletAddrStr := k.X()[0]
walletAddrStr, err := k.X(network.HRP())
if err != nil {
return err
}

for _, host := range hosts {
host.CreateStakingFiles()
host.SetNodeID()
}

// exclude API nodes from genesis file generation as they will have no stake
hostsAPI := utils.Filter(hosts, func(h *Node) bool {
return slices.Contains(maps.Keys(apiNodeIPMap), h.GetCloudID())
})
hostsWithoutAPI := utils.Filter(hosts, func(h *Node) bool {
return !slices.Contains(maps.Keys(apiNodeIPMap), h.GetCloudID())
})
hostsWithoutAPIIDs := utils.Map(hostsWithoutAPI, func(h *Node) 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(subnetName), genesisBytes, constants.WriteReadReadPerms); err != nil {
// return err
//}

// create avalanchego 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, "genesis.json")
confBytes, err := json.MarshalIndent(confMap, "", " ")
if err != nil {
return err
}

host.SetDevnetInfo(genesisBytes, confBytes)
//if err := os.WriteFile(filepath.Join(app.GetNodeInstanceDirPath(host.GetCloudID()), "genesis.json"), genesisBytes, constants.WriteReadReadPerms); err != nil {
// return err
//}
//if err := os.WriteFile(filepath.Join(app.GetNodeInstanceDirPath(host.GetCloudID()), "node.json"), 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())
bootstrapIDs = append(bootstrapIDs, host.NodeID)
bootstrapIPs = append(bootstrapIPs, fmt.Sprintf("%s:9651", host.IP))
}
}
// update node/s genesis + conf and start
wg := sync.WaitGroup{}
wgResults := NodeResults{}
for _, host := range hosts {
wg.Add(1)
go func(nodeResults *NodeResults, host *Node) {
defer wg.Done()

// keyPath := filepath.Join(app.GetNodesDir(), host.GetCloudID())
if err := host.RunSSHSetupDevNet(); 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.HasNodeIDWithError(node.NodeID) {
// ux.Logger.RedXToUser("Node %s is ERROR with error: %s", node.NodeID, wgResults.GetErrorHostMap()[node.NodeID])
fmt.Printf("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)
fmt.Printf("Node %s[%s] is SETUP as devnet", node.GetCloudID(), node.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()
fmt.Printf("Devnet Network Id: %s", logging.Green.Wrap(strconv.FormatUint(uint64(network.ID), 10)))
fmt.Printf("Devnet Endpoint: %s", logging.Green.Wrap(network.Endpoint))
// 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)
return nil
}

func generateCustomGenesis(
networkID uint32,
walletAddr string,
stakingAddr string,
hosts []*Node,
) ([]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 := bls.SecretKeyFromBytes(host.StakingFiles.BlsSignerKeyBytes)
if err != nil {
return nil, err
}
p := signer.NewProofOfPossession(blsSk)
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,
// },
//}
initialStaker := map[string]interface{}{
"nodeID": host.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{}{
"avaxAddr": 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{}{
"avaxAddr": 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 generateCustomCchainGenesis() ([]byte, error) {
cChainGenesisMap := map[string]interface{}{}
cChainGenesisMap["config"] = coreth_params.AvalancheLocalChainConfig
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 getNodeID(nodeDir string) (ids.NodeID, error) {
// certBytes, err := os.ReadFile(filepath.Join(nodeDir, constants.StakerCertFileName))
// if err != nil {
// return ids.EmptyNodeID, err
// }
// nodeID, err := utils.ToNodeID(certBytes)
// if err != nil {
// return ids.EmptyNodeID, err
// }
// return nodeID, nil
//}
Loading
Loading