mirror of
https://github.com/luxfi/netrunner.git
synced 2026-07-27 00:04:23 +00:00
330 lines
11 KiB
Go
330 lines
11 KiB
Go
package local
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"math"
|
|
"math/rand"
|
|
"net"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"github.com/luxfi/config"
|
|
"github.com/luxfi/constants"
|
|
log "github.com/luxfi/log"
|
|
"github.com/luxfi/netrunner/network/node"
|
|
"github.com/luxfi/netrunner/utils"
|
|
)
|
|
|
|
func init() {
|
|
rand.Seed(time.Now().UnixNano())
|
|
}
|
|
|
|
const (
|
|
MaxPort = math.MaxUint16
|
|
minPort = 10000
|
|
netListenTimeout = 3 * time.Second
|
|
)
|
|
|
|
// isFreePort verifies a given [port] is free
|
|
func isFreePort(port uint16) error {
|
|
// Verify it's free by binding to it
|
|
l, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
|
|
if err != nil {
|
|
// Could not bind to [port]. Assumed to be not free.
|
|
return err
|
|
}
|
|
// We could bind to [port] so must be free.
|
|
_ = l.Close()
|
|
return nil
|
|
}
|
|
|
|
// getFreePort generates a random port number and then
|
|
// verifies it is free. If it is, returns that port, otherwise retries.
|
|
// Returns an error if no free port is found within [netListenTimeout].
|
|
// Note that it is possible for [getFreePort] to return the same port twice.
|
|
func getFreePort() (uint16, error) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), netListenTimeout)
|
|
defer cancel()
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return 0, ctx.Err()
|
|
default:
|
|
// Generate random port in [minPort, maxPort]
|
|
port := uint16(rand.Intn(MaxPort-minPort+1) + minPort) //nolint
|
|
if isFreePort(port) != nil {
|
|
// Not free. Try another.
|
|
continue
|
|
}
|
|
return port, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
// writeFiles writes the files a node needs on startup.
|
|
// It returns flags used to point to those files.
|
|
func writeFiles(networkID uint32, genesis []byte, nodeRootDir string, nodeConfig *node.Config) (map[string]string, error) {
|
|
type file struct {
|
|
pathKey string
|
|
flagValue string
|
|
path string
|
|
contents []byte
|
|
}
|
|
decodedStakingSigningKey, err := base64.StdEncoding.DecodeString(nodeConfig.StakingSigningKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
files := []file{
|
|
{
|
|
flagValue: filepath.Join(nodeRootDir, stakingKeyFileName),
|
|
path: filepath.Join(nodeRootDir, stakingKeyFileName),
|
|
pathKey: config.StakingTLSKeyPathKey,
|
|
contents: []byte(nodeConfig.StakingKey),
|
|
},
|
|
{
|
|
flagValue: filepath.Join(nodeRootDir, stakingCertFileName),
|
|
path: filepath.Join(nodeRootDir, stakingCertFileName),
|
|
pathKey: config.StakingCertPathKey,
|
|
contents: []byte(nodeConfig.StakingCert),
|
|
},
|
|
{
|
|
flagValue: filepath.Join(nodeRootDir, stakingSigningKeyFileName),
|
|
path: filepath.Join(nodeRootDir, stakingSigningKeyFileName),
|
|
pathKey: config.StakingSignerKeyPathKey,
|
|
contents: decodedStakingSigningKey,
|
|
},
|
|
}
|
|
// Check if this is a resume from existing state:
|
|
// If genesis.json already exists AND db directory has chain data, preserve existing genesis
|
|
// This prevents the "db contains invalid genesis hash" error when resuming from snapshots
|
|
// EXCEPTION: For canonical networks (mainnet/testnet), if the existing genesis has a different
|
|
// networkID, we must clean the db and write the correct canonical genesis.
|
|
existingGenesisPath := filepath.Join(nodeRootDir, genesisFileName)
|
|
dbDir := filepath.Join(nodeRootDir, "db")
|
|
shouldPreserveGenesis := false
|
|
|
|
if _, err := os.Stat(existingGenesisPath); err == nil {
|
|
// Genesis exists, check if db has data (indicating we're resuming)
|
|
if entries, err := os.ReadDir(dbDir); err == nil && len(entries) > 0 {
|
|
shouldPreserveGenesis = true
|
|
|
|
// For canonical networks, verify the existing genesis matches
|
|
if networkID == constants.MainnetID || networkID == constants.TestnetID {
|
|
existingGenesisBytes, readErr := os.ReadFile(existingGenesisPath)
|
|
if readErr == nil {
|
|
existingNetworkID, parseErr := utils.NetworkIDFromGenesis(existingGenesisBytes)
|
|
if parseErr == nil && existingNetworkID != networkID {
|
|
// NetworkID mismatch! Clean db and write correct canonical genesis
|
|
log.Info("Genesis networkID mismatch for canonical network, cleaning db",
|
|
log.Uint32("existing", existingNetworkID),
|
|
log.Uint32("target", networkID),
|
|
log.String("dbDir", dbDir))
|
|
if err := os.RemoveAll(dbDir); err != nil {
|
|
return nil, fmt.Errorf("failed to clean db directory for canonical network: %w", err)
|
|
}
|
|
// Recreate empty db directory
|
|
if err := os.MkdirAll(dbDir, 0o750); err != nil {
|
|
return nil, fmt.Errorf("failed to recreate db directory: %w", err)
|
|
}
|
|
shouldPreserveGenesis = false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(genesis) > 0 && !shouldPreserveGenesis {
|
|
// Write new genesis (fresh start or custom genesis with mnemonic-derived validators)
|
|
files = append(files, file{
|
|
flagValue: existingGenesisPath,
|
|
path: existingGenesisPath,
|
|
pathKey: config.GenesisFileKey,
|
|
contents: genesis,
|
|
})
|
|
}
|
|
if len(nodeConfig.ConfigFile) != 0 {
|
|
files = append(files, file{
|
|
flagValue: filepath.Join(nodeRootDir, configFileName),
|
|
path: filepath.Join(nodeRootDir, configFileName),
|
|
pathKey: config.ConfigFileKey,
|
|
contents: []byte(nodeConfig.ConfigFile),
|
|
})
|
|
}
|
|
flags := map[string]string{}
|
|
for _, f := range files {
|
|
flags[f.pathKey] = f.flagValue
|
|
if err := createFileAndWrite(f.path, f.contents); err != nil {
|
|
return nil, fmt.Errorf("couldn't write file at %q: %w", f.path, err)
|
|
}
|
|
}
|
|
// If preserving existing genesis, add the flag without writing the file
|
|
if shouldPreserveGenesis {
|
|
flags[config.GenesisFileKey] = existingGenesisPath
|
|
}
|
|
// chain configs dir
|
|
chainConfigDir := filepath.Join(nodeRootDir, chainConfigSubDir)
|
|
if err := os.MkdirAll(chainConfigDir, 0o750); err != nil {
|
|
return nil, err
|
|
}
|
|
flags[config.ChainConfigDirKey] = chainConfigDir
|
|
// P-chain configs dir
|
|
pChainConfigDir := filepath.Join(nodeRootDir, pChainConfigSubDir)
|
|
if err := os.MkdirAll(pChainConfigDir, 0o750); err != nil {
|
|
return nil, err
|
|
}
|
|
flags[config.NetConfigDirKey] = pChainConfigDir
|
|
// chain configs
|
|
for chainAlias, chainConfigFile := range nodeConfig.ChainConfigFiles {
|
|
chainConfigPath := filepath.Join(chainConfigDir, chainAlias, configFileName)
|
|
if err := createFileAndWrite(chainConfigPath, []byte(chainConfigFile)); err != nil {
|
|
return nil, fmt.Errorf("couldn't write file at %q: %w", chainConfigPath, err)
|
|
}
|
|
}
|
|
// genesis files for EVM chains
|
|
for chainAlias, genesisFile := range nodeConfig.GenesisConfigFiles {
|
|
genesisPath := filepath.Join(chainConfigDir, chainAlias, genesisFileName)
|
|
if err := createFileAndWrite(genesisPath, []byte(genesisFile)); err != nil {
|
|
return nil, fmt.Errorf("couldn't write genesis file at %q: %w", genesisPath, err)
|
|
}
|
|
}
|
|
// network upgrades
|
|
for chainAlias, chainUpgradeFile := range nodeConfig.UpgradeConfigFiles {
|
|
chainUpgradePath := filepath.Join(chainConfigDir, chainAlias, upgradeConfigFileName)
|
|
if err := createFileAndWrite(chainUpgradePath, []byte(chainUpgradeFile)); err != nil {
|
|
return nil, fmt.Errorf("couldn't write file at %q: %w", chainUpgradePath, err)
|
|
}
|
|
}
|
|
// P-chain configs
|
|
for chainID, pChainConfigFile := range nodeConfig.PChainConfigFiles {
|
|
pChainConfigPath := filepath.Join(pChainConfigDir, chainID+".json")
|
|
if err := createFileAndWrite(pChainConfigPath, []byte(pChainConfigFile)); err != nil {
|
|
return nil, fmt.Errorf("couldn't write file at %q: %w", pChainConfigPath, err)
|
|
}
|
|
}
|
|
return flags, nil
|
|
}
|
|
|
|
// getConfigEntry returns an entry in the config file if it is found, otherwise returns the default value
|
|
func getConfigEntry(
|
|
nodeConfigFlags map[string]interface{},
|
|
configFile map[string]interface{},
|
|
flag string,
|
|
defaultVal string,
|
|
) (string, error) {
|
|
var entry string
|
|
if val, ok := nodeConfigFlags[flag]; ok {
|
|
if entry, ok := val.(string); ok {
|
|
return entry, nil
|
|
}
|
|
return "", fmt.Errorf("expected node config flag %q to be string but got %T", flag, entry)
|
|
}
|
|
if val, ok := configFile[flag]; ok {
|
|
if entry, ok := val.(string); ok {
|
|
return entry, nil
|
|
}
|
|
return "", fmt.Errorf("expected config file flag %q to be string but got %T", flag, entry)
|
|
}
|
|
return defaultVal, nil
|
|
}
|
|
|
|
// getPort looks up the port config in the config file, if there is none, it tries to get a random free port from the OS
|
|
// if [reassingIfUsed] is true, and the port from config is not free, also tries to get a random free port
|
|
func getPort(
|
|
flags map[string]interface{},
|
|
configFile map[string]interface{},
|
|
portKey string,
|
|
reassignIfUsed bool,
|
|
) (port uint16, err error) {
|
|
if portIntf, ok := flags[portKey]; ok {
|
|
switch gotPort := portIntf.(type) {
|
|
case int:
|
|
port = uint16(gotPort)
|
|
fmt.Printf("🔍 getPort: %s found in flags as int=%d\n", portKey, port)
|
|
case float64:
|
|
port = uint16(gotPort)
|
|
fmt.Printf("🔍 getPort: %s found in flags as float64=%d\n", portKey, port)
|
|
default:
|
|
return 0, fmt.Errorf("expected flag %q to be int/float64 but got %T", portKey, portIntf)
|
|
}
|
|
} else if portIntf, ok := configFile[portKey]; ok {
|
|
fmt.Printf("🔍 getPort: %s NOT in flags, checking configFile\n", portKey)
|
|
portFromConfigFile, ok := portIntf.(float64)
|
|
if !ok {
|
|
return 0, fmt.Errorf("expected flag %q to be float64 but got %T", portKey, portIntf)
|
|
}
|
|
port = uint16(portFromConfigFile)
|
|
} else {
|
|
// Use a random free port.
|
|
// Note: it is possible but unlikely for getFreePort to return the same port multiple times.
|
|
fmt.Printf("🔍 getPort: %s NOT in flags or configFile, using random port\n", portKey)
|
|
port, err = getFreePort()
|
|
if err != nil {
|
|
return 0, fmt.Errorf("couldn't get free port: %w", err)
|
|
}
|
|
}
|
|
if reassignIfUsed && isFreePort(port) != nil {
|
|
fmt.Printf("🔍 getPort: %s port %d NOT free, reassigning to random\n", portKey, port)
|
|
port, err = getFreePort()
|
|
if err != nil {
|
|
return 0, fmt.Errorf("couldn't get free port: %w", err)
|
|
}
|
|
}
|
|
// last check, avoid starting network with used ports
|
|
if err := isFreePort(port); err != nil {
|
|
return 0, fmt.Errorf("port %d is not free: %w", port, err)
|
|
}
|
|
return port, nil
|
|
}
|
|
|
|
func makeNodeDir(logger log.Logger, rootDir, nodeName string) (string, error) {
|
|
if rootDir == "" {
|
|
log.Warn("no network root directory defined; will create this node's runtime directory in working directory")
|
|
}
|
|
// [nodeRootDir] is where this node's config file, C-Chain config file,
|
|
// staking key, staking certificate and genesis file will be written.
|
|
// (Other file locations are given in the node's config file.)
|
|
// TODO should we do this for other directories? Profiles?
|
|
nodeRootDir := getNodeDir(rootDir, nodeName)
|
|
if err := os.Mkdir(nodeRootDir, 0o755); err != nil {
|
|
if !os.IsExist(err) {
|
|
return "", fmt.Errorf("error creating temp dir %w", err)
|
|
}
|
|
log.Warn("node root directory already exists", log.String("root-dir", nodeRootDir))
|
|
}
|
|
return nodeRootDir, nil
|
|
}
|
|
|
|
func getNodeDir(rootDir string, nodeName string) string {
|
|
return filepath.Join(rootDir, nodeName)
|
|
}
|
|
|
|
// createFileAndWrite creates a file with the given path and
|
|
// writes the given contents
|
|
func createFileAndWrite(path string, contents []byte) error {
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o750); err != nil {
|
|
return err
|
|
}
|
|
file, err := os.Create(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer file.Close()
|
|
_, err = file.Write(contents)
|
|
return err
|
|
}
|
|
|
|
// addNetworkFlags adds the flags in [networkFlags] to [nodeConfig.Flags].
|
|
// [nodeFlags] must not be nil.
|
|
func addNetworkFlags(networkFlags map[string]interface{}, nodeFlags map[string]interface{}) {
|
|
for flagName, flagVal := range networkFlags {
|
|
// If the same flag is given in network config and node config,
|
|
// the flag in the node config takes precedence
|
|
if _, ok := nodeFlags[flagName]; !ok {
|
|
nodeFlags[flagName] = flagVal
|
|
}
|
|
}
|
|
}
|