refactor: remove all SubnetEVM references

- Rename --subnet-evm-path flag to --evm-path
- Rename subnetEvmPath variable to evmPath
- Rename subnet-evm-genesis.json to evm-genesis.json
- Change VmName from "subnetevm" to "evm"
- Update all test configurations
This commit is contained in:
Zach Kelling
2025-12-21 11:12:41 -08:00
parent 4e9393e5fb
commit 8d816a38ad
13 changed files with 801 additions and 293 deletions
+3 -3
View File
@@ -70,21 +70,21 @@ var l2Chains = []L2Chain{
{
Name: "Zoo",
ChainID: 200200,
VMName: "subnetevm",
VMName: "evm",
Alias: "zoo",
GenesisFile: "chains/zoo/genesis.json",
},
{
Name: "SPC",
ChainID: 36911,
VMName: "subnetevm",
VMName: "evm",
Alias: "spc",
GenesisFile: "chains/spc/genesis.json",
},
{
Name: "Hanzo AI",
ChainID: 36963,
VMName: "subnetevm",
VMName: "evm",
Alias: "hanzo",
GenesisFile: "chains/ai/genesis.json",
},
+466 -91
View File
@@ -9,6 +9,7 @@ import (
"encoding/hex"
"errors"
"fmt"
"net/http"
"sort"
"strings"
"time"
@@ -23,6 +24,7 @@ import (
"github.com/luxfi/netrunner/network"
"github.com/luxfi/netrunner/network/node"
"github.com/luxfi/netrunner/utils"
luxconfig "github.com/luxfi/config"
"github.com/luxfi/node/api/admin"
"github.com/luxfi/node/config"
"github.com/luxfi/crypto/secp256k1"
@@ -59,7 +61,7 @@ const (
blockchainLogPullFrequency = time.Second
// check period while waiting for all validators to be ready
waitForValidatorsPullFrequency = time.Second
defaultTimeout = time.Minute
defaultTimeout = 3 * time.Minute
stakingMinimumLeadTime = 25 * time.Second
minStakeDuration = 24 * 14 * time.Hour
)
@@ -109,17 +111,46 @@ func (ln *localNetwork) CreateChains(
ln.lock.Lock()
defer ln.lock.Unlock()
// Log the chain specs we're trying to create for debugging
for i, spec := range chainSpecs {
ln.log.Info("CreateChains: processing chain spec",
"index", i,
"vmName", spec.VMName,
"hasGenesis", len(spec.Genesis) > 0,
"hasChainID", spec.ChainID != nil,
)
}
chainInfos, err := ln.installCustomChains(ctx, chainSpecs)
if err != nil {
return nil, err
ln.log.Error("CreateChains: failed to install custom chains",
"error", err.Error(),
"numChainSpecs", len(chainSpecs),
)
return nil, fmt.Errorf("failed to install custom chains: %w", err)
}
if err := ln.waitForCustomChainsReady(ctx, chainInfos); err != nil {
return nil, err
ln.log.Error("CreateChains: chains failed to become ready",
"error", err.Error(),
"numChains", len(chainInfos),
)
return nil, fmt.Errorf("chains failed to become ready: %w", err)
}
// Wait for all nodes to discover the chains before aliasing
if err := ln.waitForChainsDiscoveredOnAllNodes(ctx, chainInfos); err != nil {
ln.log.Error("CreateChains: chains not discovered on all nodes",
"error", err.Error(),
)
return nil, fmt.Errorf("chains not discovered on all nodes: %w", err)
}
if err := ln.RegisterAliases(ctx, chainInfos, chainSpecs); err != nil {
return nil, err
ln.log.Error("CreateChains: failed to register aliases",
"error", err.Error(),
)
return nil, fmt.Errorf("failed to register aliases: %w", err)
}
chainIDs := []ids.ID{}
@@ -155,14 +186,114 @@ func (ln *localNetwork) RegisterAliases(
if adminClient == nil {
return fmt.Errorf("admin client is nil for node %v", nodeName)
}
if err := (*adminClient).AliasChain(ctx, chainID, blockchainAlias); err != nil {
return fmt.Errorf("failure to register blockchain alias %v on node %v: %w", blockchainAlias, nodeName, err)
// Retry with exponential backoff - nodes may not have discovered chain yet
maxRetries := 10
baseDelay := 500 * time.Millisecond
var lastErr error
for attempt := 0; attempt < maxRetries; attempt++ {
if err := (*adminClient).AliasChain(ctx, chainID, blockchainAlias); err != nil {
lastErr = err
delay := baseDelay * time.Duration(1<<attempt)
if delay > 5*time.Second {
delay = 5 * time.Second
}
ln.log.Debug("alias registration failed, retrying",
"node", nodeName,
"chain", chainID,
"attempt", attempt+1,
"delay", delay,
"error", err.Error(),
)
select {
case <-ctx.Done():
return fmt.Errorf("context cancelled while registering alias: %w", ctx.Err())
case <-time.After(delay):
continue
}
}
lastErr = nil
break
}
if lastErr != nil {
return fmt.Errorf("failure to register blockchain alias %v on node %v after %d attempts: %w",
blockchainAlias, nodeName, maxRetries, lastErr)
}
}
}
return nil
}
// waitForChainsDiscoveredOnAllNodes waits until all nodes recognize the blockchain IDs
// This is needed because even with track-chains=all, nodes need time to sync P-Chain
// and discover newly created chains
func (ln *localNetwork) waitForChainsDiscoveredOnAllNodes(
ctx context.Context,
chainInfos []blockchainInfo,
) error {
fmt.Println()
ln.log.Info(luxlog.Blue.Wrap(luxlog.Bold.Wrap("waiting for chains to be discovered on all nodes...")))
maxWait := 60 * time.Second
pollInterval := 2 * time.Second
deadline := time.Now().Add(maxWait)
for _, chainInfo := range chainInfos {
blockchainID := chainInfo.blockchainID.String()
ln.log.Info("waiting for chain discovery",
"blockchain-id", blockchainID,
)
for nodeName, node := range ln.nodes {
if node.paused {
continue
}
discovered := false
for time.Now().Before(deadline) {
// Try to query the chain RPC endpoint - if it responds, the chain is discovered
chainRPCURL := fmt.Sprintf("%s/ext/bc/%s/rpc", node.GetURL(), blockchainID)
req, err := http.NewRequestWithContext(ctx, "POST", chainRPCURL, nil)
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
client := &http.Client{Timeout: 5 * time.Second}
resp, err := client.Do(req)
if err == nil {
resp.Body.Close()
// Any response (even error) means the chain endpoint exists
discovered = true
ln.log.Debug("chain discovered on node",
"node", nodeName,
"blockchain-id", blockchainID,
)
break
}
ln.log.Debug("chain not yet discovered, waiting",
"node", nodeName,
"blockchain-id", blockchainID,
)
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(pollInterval):
continue
}
}
if !discovered {
return fmt.Errorf("chain %s not discovered on node %s after %v", blockchainID, nodeName, maxWait)
}
}
}
ln.log.Info(luxlog.Green.Wrap("all chains discovered on all nodes"))
return nil
}
func (ln *localNetwork) RemoveChainValidators(
ctx context.Context,
removeParticipantsSpecs []network.RemoveChainValidatorSpec,
@@ -215,12 +346,18 @@ func (ln *localNetwork) installCustomChains(
// Ensure nodes are healthy before proceeding (nodes may have been restarted by a prior
// CreateParticipantGroups call which restarts nodes to track chains)
if err := ln.healthy(ctx); err != nil {
ln.log.Error("installCustomChains: network not healthy before chain installation",
"error", err.Error(),
)
return nil, fmt.Errorf("network not healthy at start of installCustomChains: %w", err)
}
clientURI, err := ln.getClientURI()
if err != nil {
return nil, err
ln.log.Error("installCustomChains: failed to get client URI",
"error", err.Error(),
)
return nil, fmt.Errorf("failed to get client URI: %w", err)
}
platformCli := platformvm.NewClient(clientURI)
@@ -234,7 +371,12 @@ func (ln *localNetwork) installCustomChains(
if chainSpec.ChainID != nil {
chainID, err := ids.FromString(*chainSpec.ChainID)
if err != nil {
return nil, err
ln.log.Error("installCustomChains: invalid chain ID format",
"error", err.Error(),
"chainID", *chainSpec.ChainID,
"vmName", chainSpec.VMName,
)
return nil, fmt.Errorf("invalid chain ID %q for VM %s: %w", *chainSpec.ChainID, chainSpec.VMName, err)
}
preloadTXs = append(preloadTXs, chainID)
}
@@ -242,7 +384,11 @@ func (ln *localNetwork) installCustomChains(
w, err := newWallet(ctx, clientURI, preloadTXs)
if err != nil {
return nil, err
ln.log.Error("installCustomChains: failed to create wallet",
"error", err.Error(),
"clientURI", clientURI,
)
return nil, fmt.Errorf("failed to create wallet for chain installation: %w", err)
}
// get chain specs for all new chains to create
@@ -276,28 +422,45 @@ func (ln *localNetwork) installCustomChains(
if !ok {
ln.log.Info(luxlog.Green.Wrap(fmt.Sprintf("adding new participant %s", nodeName)))
if _, err := ln.addNode(node.Config{Name: nodeName}); err != nil {
return nil, err
ln.log.Error("installCustomChains: failed to add participant node",
"error", err.Error(),
"nodeName", nodeName,
)
return nil, fmt.Errorf("failed to add participant node %s: %w", nodeName, err)
}
}
}
}
if err := ln.healthy(ctx); err != nil {
return nil, err
ln.log.Error("installCustomChains: network not healthy after adding nodes",
"error", err.Error(),
)
return nil, fmt.Errorf("network not healthy after adding nodes: %w", err)
}
// just ensure all nodes are primary validators (so can be chain validators)
if err := ln.addPrimaryValidators(ctx, platformCli, w); err != nil {
return nil, err
ln.log.Error("installCustomChains: failed to add primary validators",
"error", err.Error(),
)
return nil, fmt.Errorf("failed to add primary validators: %w", err)
}
// create missing chains
chainIDs, err := createChains(ctx, uint32(len(participantsSpecs)), w, ln.log)
if err != nil {
return nil, err
ln.log.Error("installCustomChains: failed to create chains",
"error", err.Error(),
"numChains", len(participantsSpecs),
)
return nil, fmt.Errorf("failed to create chains: %w", err)
}
if err := ln.setChainConfigFiles(chainIDs, participantsSpecs); err != nil {
return nil, err
ln.log.Error("installCustomChains: failed to set chain config files",
"error", err.Error(),
)
return nil, fmt.Errorf("failed to set chain config files: %w", err)
}
// assign created chains to blockchain requests with undefined chain id
@@ -312,59 +475,117 @@ func (ln *localNetwork) installCustomChains(
// wait for nodes to be primary validators before trying to add them as chain ones
if err = ln.waitPrimaryValidators(ctx, platformCli); err != nil {
return nil, err
ln.log.Error("installCustomChains: timeout waiting for primary validators",
"error", err.Error(),
)
return nil, fmt.Errorf("timeout waiting for primary validators: %w", err)
}
if err = ln.addChainValidators(ctx, platformCli, w, chainIDs, participantsSpecs); err != nil {
return nil, err
ln.log.Error("installCustomChains: failed to add chain validators",
"error", err.Error(),
)
return nil, fmt.Errorf("failed to add chain validators: %w", err)
}
blockchainTxs, err := createBlockchainTxs(ctx, chainSpecs, w, ln.log)
if err != nil {
return nil, err
ln.log.Error("installCustomChains: failed to create blockchain transactions",
"error", err.Error(),
)
return nil, fmt.Errorf("failed to create blockchain transactions: %w", err)
}
nodesToRestartForBlockchainConfigUpdate, err := ln.setBlockchainConfigFiles(ctx, chainSpecs, blockchainTxs, chainIDs, participantsSpecs, ln.log)
if err != nil {
return nil, err
ln.log.Error("installCustomChains: failed to set blockchain config files",
"error", err.Error(),
)
return nil, fmt.Errorf("failed to set blockchain config files: %w", err)
}
if len(participantsSpecs) > 0 || len(nodesToRestartForBlockchainConfigUpdate) > 0 {
// we need to restart if there are new chains or if there are new network config files
// add missing chains, restarting network and waiting for chain validation to start
// First try to hot-reload VM plugins without restart
// This is the preferred path - no node restart needed
if err := ln.reloadVMPlugins(ctx); err != nil {
ln.log.Warn("VM hot-reload failed, will try restart path",
"error", err.Error(),
"errorDetail", fmt.Sprintf("%+v", err),
)
}
// Only restart if there are config file updates that require restart
// Skip restart for new chains - VMs are hot-loaded above
if len(nodesToRestartForBlockchainConfigUpdate) > 0 {
ln.log.Info("restarting nodes for config file updates",
"numNodesToRestart", len(nodesToRestartForBlockchainConfigUpdate),
)
if err := ln.restartNodes(ctx, chainIDs, participantsSpecs, nil, nil, nodesToRestartForBlockchainConfigUpdate); err != nil {
return nil, err
ln.log.Error("installCustomChains: failed to restart nodes for config update",
"error", err.Error(),
"nodesToRestart", nodesToRestartForBlockchainConfigUpdate.List(),
)
return nil, fmt.Errorf("failed to restart nodes for config update: %w", err)
}
clientURI, err = ln.getClientURI()
if err != nil {
return nil, err
ln.log.Error("installCustomChains: failed to get client URI after restart",
"error", err.Error(),
)
return nil, fmt.Errorf("failed to get client URI after restart: %w", err)
}
w.reload(clientURI)
}
// refresh vm list
if err := ln.reloadVMPlugins(ctx); err != nil {
return nil, err
// Reload VMs again after restart
if err := ln.reloadVMPlugins(ctx); err != nil {
ln.log.Error("installCustomChains: failed to reload VM plugins after restart",
"error", err.Error(),
"errorDetail", fmt.Sprintf("%+v", err),
)
return nil, fmt.Errorf("failed to reload VM plugins after restart: %w", err)
}
}
if err = ln.waitChainValidators(ctx, platformCli, chainIDs, participantsSpecs); err != nil {
return nil, err
ln.log.Error("installCustomChains: timeout waiting for chain validators",
"error", err.Error(),
)
return nil, fmt.Errorf("timeout waiting for chain validators: %w", err)
}
// create blockchain from txs before spending more utxos
if err := ln.createChains(ctx, chainSpecs, blockchainTxs, w, ln.log); err != nil {
return nil, err
ln.log.Error("installCustomChains: failed to create blockchains from transactions",
"error", err.Error(),
"numChainSpecs", len(chainSpecs),
)
return nil, fmt.Errorf("failed to create blockchains from transactions: %w", err)
}
// With track-all-chains=true, nodes auto-discover new chains via hot-load
// Reload VMs to ensure any new VMs are available
if err := ln.reloadVMPlugins(ctx); err != nil {
ln.log.Warn("VM hot-reload after chain creation failed (non-fatal)",
"error", err.Error(),
)
}
chainInfos := make([]blockchainInfo, len(chainSpecs))
for i, chainSpec := range chainSpecs {
vmID, err := utils.VMID(chainSpec.VMName)
if err != nil {
return nil, err
ln.log.Error("installCustomChains: invalid VM name",
"error", err.Error(),
"vmName", chainSpec.VMName,
)
return nil, fmt.Errorf("invalid VM name %s: %w", chainSpec.VMName, err)
}
chainID, err := ids.FromString(*chainSpec.ChainID)
if err != nil {
return nil, err
ln.log.Error("installCustomChains: invalid chain ID",
"error", err.Error(),
"chainID", *chainSpec.ChainID,
"vmName", chainSpec.VMName,
)
return nil, fmt.Errorf("invalid chain ID %s for VM %s: %w", *chainSpec.ChainID, chainSpec.VMName, err)
}
chainInfos[i] = blockchainInfo{
// we keep a record of VM name in blockchain name field,
@@ -472,8 +693,9 @@ func (ln *localNetwork) getChainValidatorsNodenames(
}
platformCli := platformvm.NewClient(clientURI)
ctx, cancel := createDefaultCtx(ctx)
vs, err := platformCli.GetCurrentValidators(ctx, chainID, nil)
// Use different variable name to avoid shadowing outer context
apiCtx, cancel := createDefaultCtx(ctx)
vs, err := platformCli.GetCurrentValidators(apiCtx, chainID, nil)
cancel()
if err != nil {
return nil, err
@@ -548,11 +770,11 @@ func (ln *localNetwork) restartNodes(
nodeConfig := node.GetConfig()
previousTrackedChains := ""
previousTrackedChainsIntf, ok := nodeConfig.Flags[config.TrackNetsKey]
previousTrackedChainsIntf, ok := nodeConfig.Flags[config.TrackChainsKey]
if ok {
previousTrackedChains, ok = previousTrackedChainsIntf.(string)
if !ok {
return fmt.Errorf("expected node config %s to have type string obtained %T", config.TrackNetsKey, previousTrackedChainsIntf)
return fmt.Errorf("expected node config %s to have type string obtained %T", config.TrackChainsKey, previousTrackedChainsIntf)
}
}
@@ -592,7 +814,7 @@ func (ln *localNetwork) restartNodes(
sort.Strings(trackChainIDs)
tracked := strings.Join(trackChainIDs, ",")
nodeConfig.Flags[config.TrackNetsKey] = tracked
nodeConfig.Flags[config.TrackChainsKey] = tracked
if participantsSpecs != nil {
if nodesToRestartForBlockchainConfigUpdate.Contains(nodeName) {
@@ -734,8 +956,9 @@ func (ln *localNetwork) addPrimaryValidators(
) error {
ln.log.Info(luxlog.Green.Wrap("adding the nodes as primary network validators"))
// ref. https://docs.lux.network/build/node-apis/p-chain/#platformgetcurrentvalidators
ctx, cancel := createDefaultCtx(ctx)
vdrs, err := platformCli.GetCurrentValidators(ctx, constants.PrimaryNetworkID, nil)
// Use different variable name to avoid shadowing outer context
apiCtx, cancel := createDefaultCtx(ctx)
vdrs, err := platformCli.GetCurrentValidators(apiCtx, constants.PrimaryNetworkID, nil)
cancel()
if err != nil {
return err
@@ -766,7 +989,8 @@ func (ln *localNetwork) addPrimaryValidators(
if err != nil {
return err
}
ctx, cancel = createDefaultCtx(ctx)
// Use different variable name to avoid shadowing outer context
txCtx, txCancel := createDefaultCtx(ctx)
tx, err := w.pWallet.IssueAddPermissionlessValidatorTx(
&txs.ChainValidator{
Validator: txs.Validator{
@@ -788,10 +1012,10 @@ func (ln *localNetwork) addPrimaryValidators(
Addrs: []ids.ShortID{w.addr},
},
10*10000, // 10% fee percent, times 10000 to make it as shares
common.WithContext(ctx),
common.WithContext(txCtx),
defaultPoll,
)
cancel()
txCancel()
if err != nil {
return fmt.Errorf("P-Wallet Tx Error %s %w, node ID %s", "IssueAddPermissionlessValidatorTx", err, nodeID.String())
}
@@ -807,7 +1031,8 @@ func getXChainAssetID(ctx context.Context, w *wallet, tokenName string, tokenSym
w.addr,
},
}
ctx, cancel := createDefaultCtx(ctx)
// Use different variable name to avoid shadowing outer context
txCtx, cancel := createDefaultCtx(ctx)
defer cancel()
tx, err := w.xWallet.IssueCreateAssetTx(
tokenName,
@@ -821,7 +1046,7 @@ func getXChainAssetID(ctx context.Context, w *wallet, tokenName string, tokenSym
},
},
},
common.WithContext(ctx),
common.WithContext(txCtx),
defaultPoll,
)
if err != nil {
@@ -831,7 +1056,8 @@ func getXChainAssetID(ctx context.Context, w *wallet, tokenName string, tokenSym
}
func exportXChainToPChain(ctx context.Context, w *wallet, owner *secp256k1fx.OutputOwners, chainAssetID ids.ID, assetAmount uint64) error {
ctx, cancel := createDefaultCtx(ctx)
// Use different variable name to avoid shadowing outer context
txCtx, cancel := createDefaultCtx(ctx)
defer cancel()
_, err := w.xWallet.IssueExportTx(
ids.Empty,
@@ -846,7 +1072,7 @@ func exportXChainToPChain(ctx context.Context, w *wallet, owner *secp256k1fx.Out
},
},
},
common.WithContext(ctx),
common.WithContext(txCtx),
defaultPoll,
)
return err
@@ -854,12 +1080,13 @@ func exportXChainToPChain(ctx context.Context, w *wallet, owner *secp256k1fx.Out
func importPChainFromXChain(ctx context.Context, w *wallet, owner *secp256k1fx.OutputOwners, xChainID ids.ID) error {
pWallet := w.pWallet
ctx, cancel := createDefaultCtx(ctx)
// Use different variable name to avoid shadowing outer context
txCtx, cancel := createDefaultCtx(ctx)
defer cancel()
_, err := pWallet.IssueImportTx(
xChainID,
owner,
common.WithContext(ctx),
common.WithContext(txCtx),
defaultPoll,
)
return err
@@ -895,9 +1122,10 @@ func (ln *localNetwork) removeChainValidators(
if err != nil {
return err
}
ctx, cancel := createDefaultCtx(ctx)
vs, err := platformCli.GetCurrentValidators(ctx, chainID, nil)
cancel()
// Use different variable name to avoid shadowing outer context
apiCtx, apiCancel := createDefaultCtx(ctx)
vs, err := platformCli.GetCurrentValidators(apiCtx, chainID, nil)
apiCancel()
if err != nil {
return err
}
@@ -915,14 +1143,15 @@ func (ln *localNetwork) removeChainValidators(
if isValidator := chainValidators.Contains(nodeID); !isValidator {
return fmt.Errorf("node %s is currently not a chain validator of chain %s", nodeName, chainID.String())
}
ctx, cancel := createDefaultCtx(ctx)
// Use different variable name to avoid shadowing outer context
txCtx, txCancel := createDefaultCtx(ctx)
tx, err := w.pWallet.IssueRemoveChainValidatorTx(
nodeID,
chainID,
common.WithContext(ctx),
common.WithContext(txCtx),
defaultPoll,
)
cancel()
txCancel()
if err != nil {
return fmt.Errorf("P-Wallet Tx Error %s %w, node ID %s, chainID %s", "IssueRemoveChainValidatorTx", err, nodeID.String(), chainID.String())
}
@@ -991,8 +1220,9 @@ func (ln *localNetwork) addPermissionlessValidators(
return err
}
ctx, cancel := createDefaultCtx(ctx)
vs, err := platformCli.GetCurrentValidators(ctx, constants.PrimaryNetworkID, nil)
// Use different variable name to avoid shadowing outer context
apiCtx, cancel := createDefaultCtx(ctx)
vs, err := platformCli.GetCurrentValidators(apiCtx, constants.PrimaryNetworkID, nil)
cancel()
if err != nil {
return err
@@ -1004,14 +1234,17 @@ func (ln *localNetwork) addPermissionlessValidators(
for _, validatorSpec := range validatorSpecs {
ln.log.Info(luxlog.Green.Wrap("adding permissionless validator"), "node ", validatorSpec.NodeName)
ctx, cancel := createDefaultCtx(ctx)
// Use different variable name to avoid shadowing outer context
txCtx, txCancel := createDefaultCtx(ctx)
validatorNodeID := ln.nodes[validatorSpec.NodeName].nodeID
chainID, err := ids.FromString(validatorSpec.ChainID)
if err != nil {
txCancel()
return err
}
assetID, err := ids.FromString(validatorSpec.AssetID)
if err != nil {
txCancel()
return err
}
var startTime uint64
@@ -1042,10 +1275,10 @@ func (ln *localNetwork) addPermissionlessValidators(
owner,
&secp256k1fx.OutputOwners{},
reward.PercentDenominator,
common.WithContext(ctx),
common.WithContext(txCtx),
defaultPoll,
)
cancel()
txCancel()
if err != nil {
return err
}
@@ -1112,16 +1345,17 @@ func (ln *localNetwork) transformToElasticChains(
if err != nil {
return nil, nil, err
}
ctx, cancel := createDefaultCtx(ctx)
// Use different variable name to avoid shadowing outer context
txCtx, txCancel := createDefaultCtx(ctx)
transformChainTx, err := w.pWallet.IssueTransformChainTx(chainID, chainAssetID,
elasticParticipantsSpec.InitialSupply, elasticParticipantsSpec.MaxSupply, elasticParticipantsSpec.MinConsumptionRate,
elasticParticipantsSpec.MaxConsumptionRate, elasticParticipantsSpec.MinValidatorStake, elasticParticipantsSpec.MaxValidatorStake,
elasticParticipantsSpec.MinStakeDuration, elasticParticipantsSpec.MaxStakeDuration, elasticParticipantsSpec.MinDelegationFee,
elasticParticipantsSpec.MinDelegatorStake, elasticParticipantsSpec.MaxValidatorWeightFactor, elasticParticipantsSpec.UptimeRequirement,
common.WithContext(ctx),
common.WithContext(txCtx),
defaultPoll,
)
cancel()
txCancel()
if err != nil {
return nil, nil, err
}
@@ -1151,16 +1385,17 @@ func createChains(
chainIDs := make([]ids.ID, numChains)
for i := uint32(0); i < numChains; i++ {
log.Info("creating chain tx")
ctx, cancel := createDefaultCtx(ctx)
// Use different variable name to avoid shadowing outer context
txCtx, txCancel := createDefaultCtx(ctx)
tx, err := w.pWallet.IssueCreateSubnetTx(
&secp256k1fx.OutputOwners{
Threshold: 1,
Addrs: []ids.ShortID{w.addr},
},
common.WithContext(ctx),
common.WithContext(txCtx),
defaultPoll,
)
cancel()
txCancel()
if err != nil {
return nil, fmt.Errorf("P-Wallet Tx Error %s %w", "IssueCreateChainTx", err)
}
@@ -1185,9 +1420,10 @@ func (ln *localNetwork) addChainValidators(
ln.log.Info(luxlog.Green.Wrap("adding the nodes as chain validators"))
for i, chainID := range chainIDs {
ln.log.Info("getting primary validators for chain", "index", i, "chain-ID", chainID.String())
ctx, cancel := createDefaultCtx(ctx)
vs, err := platformCli.GetCurrentValidators(ctx, constants.PrimaryNetworkID, nil)
cancel()
// Use different variable name to avoid shadowing outer context
apiCtx1, cancel1 := createDefaultCtx(ctx)
vs, err := platformCli.GetCurrentValidators(apiCtx1, constants.PrimaryNetworkID, nil)
cancel1()
if err != nil {
ln.log.Error("failed to get primary validators", "error", err.Error())
return fmt.Errorf("failed to get primary validators: %w", err)
@@ -1198,9 +1434,10 @@ func (ln *localNetwork) addChainValidators(
primaryValidatorsEndtime[v.NodeID] = time.Unix(int64(v.EndTime), 0)
}
ln.log.Info("getting current validators for chain", "chain-ID", chainID.String())
ctx, cancel = createDefaultCtx(ctx)
vs, err = platformCli.GetCurrentValidators(ctx, chainID, nil)
cancel()
// Use different variable name to avoid shadowing outer context
apiCtx2, cancel2 := createDefaultCtx(ctx)
vs, err = platformCli.GetCurrentValidators(apiCtx2, chainID, nil)
cancel2()
if err != nil {
ln.log.Error("failed to get current validators for chain", "chain-ID", chainID.String(), "error", err.Error())
return fmt.Errorf("failed to get current validators for chain %s: %w", chainID.String(), err)
@@ -1220,7 +1457,8 @@ func (ln *localNetwork) addChainValidators(
if isValidator := chainValidators.Contains(nodeID); isValidator {
continue
}
ctx, cancel := createDefaultCtx(ctx)
// Use different variable name to avoid shadowing outer context
txCtx, txCancel := createDefaultCtx(ctx)
tx, err := w.pWallet.IssueAddChainValidatorTx(
&txs.ChainValidator{
Validator: txs.Validator{
@@ -1232,10 +1470,10 @@ func (ln *localNetwork) addChainValidators(
},
Chain: chainID,
},
common.WithContext(ctx),
common.WithContext(txCtx),
defaultPoll,
)
cancel()
txCancel()
if err != nil {
return fmt.Errorf("P-Wallet Tx Error %s %w, node ID %s, chainID %s", "IssueAddChainValidatorTx", err, nodeID.String(), chainID.String())
}
@@ -1258,8 +1496,9 @@ func (ln *localNetwork) waitPrimaryValidators(
ln.log.Info(luxlog.Green.Wrap("waiting for the nodes to become primary validators"))
for {
ready := true
ctx, cancel := createDefaultCtx(ctx)
vs, err := platformCli.GetCurrentValidators(ctx, constants.PrimaryNetworkID, nil)
// Use a different variable name to avoid shadowing the outer context
apiCtx, cancel := createDefaultCtx(ctx)
vs, err := platformCli.GetCurrentValidators(apiCtx, constants.PrimaryNetworkID, nil)
cancel()
if err != nil {
return err
@@ -1298,8 +1537,9 @@ func (ln *localNetwork) waitChainValidators(
for {
ready := true
for i, chainID := range chainIDs {
ctx, cancel := createDefaultCtx(ctx)
vs, err := platformCli.GetCurrentValidators(ctx, chainID, nil)
// Use a different variable name to avoid shadowing the outer context
apiCtx, cancel := createDefaultCtx(ctx)
vs, err := platformCli.GetCurrentValidators(apiCtx, chainID, nil)
cancel()
if err != nil {
return err
@@ -1336,25 +1576,141 @@ func (ln *localNetwork) waitChainValidators(
}
}
// reload VM plugins on all nodes
// reload VM plugins on all nodes and verify they're available
func (ln *localNetwork) reloadVMPlugins(ctx context.Context) error {
ln.log.Info(luxlog.Green.Wrap("reloading plugin binaries"))
for _, node := range ln.nodes {
// Use unified config to resolve plugin directory for diagnostics
pluginDir := luxconfig.ResolvePluginDir()
ln.log.Info(luxlog.Green.Wrap("reloading plugin binaries"),
"pluginDir", pluginDir,
)
// Track newly loaded VMs for verification
var newlyLoadedVMs []ids.ID
for nodeName, node := range ln.nodes {
if node.paused {
continue
}
uri := fmt.Sprintf("http://%s:%d", node.GetURL(), node.GetAPIPort())
adminCli := admin.NewClient(uri)
ctx, cancel := createDefaultCtx(ctx)
_, failedVMs, err := adminCli.LoadVMs(ctx)
// Use different variable name to avoid shadowing outer context
apiCtx, cancel := createDefaultCtx(ctx)
loadedVMs, failedVMs, err := adminCli.LoadVMs(apiCtx)
cancel()
if err != nil {
return err
ln.log.Error("reloadVMPlugins: failed to load VMs on node",
"error", err.Error(),
"nodeName", nodeName,
"nodeURI", uri,
"pluginDir", pluginDir,
)
return fmt.Errorf("failed to load VMs on node %s (%s): %w (plugin dir: %s)", nodeName, uri, err, pluginDir)
}
if len(failedVMs) > 0 {
return fmt.Errorf("%d VMs failed to load: %v", len(failedVMs), failedVMs)
// Log detailed information about each failed VM
for vmID, vmErr := range failedVMs {
ln.log.Error("reloadVMPlugins: VM failed to load",
"vmID", vmID,
"error", vmErr,
"nodeName", nodeName,
"nodeURI", uri,
"pluginDir", pluginDir,
)
}
return fmt.Errorf("%d VMs failed to load on node %s: %v (plugin dir: %s)", len(failedVMs), nodeName, failedVMs, pluginDir)
}
if len(loadedVMs) > 0 {
ln.log.Info("reloadVMPlugins: successfully loaded VMs",
"nodeName", nodeName,
"loadedVMs", loadedVMs,
"pluginDir", pluginDir,
)
// Collect newly loaded VMs for verification
for vmID := range loadedVMs {
newlyLoadedVMs = append(newlyLoadedVMs, vmID)
}
}
}
// If we loaded new VMs, verify they're actually available on all nodes
if len(newlyLoadedVMs) > 0 {
if err := ln.verifyVMsAvailable(ctx, newlyLoadedVMs); err != nil {
return fmt.Errorf("VMs loaded but not available: %w", err)
}
}
return nil
}
// verifyVMsAvailable checks that all specified VMs are available on all nodes
// with retry logic to handle hot-loading delays
func (ln *localNetwork) verifyVMsAvailable(ctx context.Context, vmIDs []ids.ID) error {
const (
maxRetries = 10
retryInterval = 500 * time.Millisecond
)
for nodeName, node := range ln.nodes {
if node.paused {
continue
}
uri := fmt.Sprintf("http://%s:%d", node.GetURL(), node.GetAPIPort())
adminCli := admin.NewClient(uri)
for _, vmID := range vmIDs {
var lastErr error
available := false
for attempt := 0; attempt < maxRetries; attempt++ {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
apiCtx, cancel := createDefaultCtx(ctx)
registeredVMs, err := adminCli.ListVMs(apiCtx)
cancel()
if err != nil {
lastErr = err
time.Sleep(retryInterval)
continue
}
// Check if our VM is in the list
vmIDStr := vmID.String()
if _, exists := registeredVMs[vmIDStr]; exists {
available = true
break
}
lastErr = fmt.Errorf("VM %s not found in registered VMs", vmIDStr)
time.Sleep(retryInterval)
}
if !available {
ln.log.Error("verifyVMsAvailable: VM not available after retries",
"vmID", vmID.String(),
"nodeName", nodeName,
"nodeURI", uri,
"maxRetries", maxRetries,
"lastError", lastErr,
)
return fmt.Errorf("VM %s not available on node %s after %d attempts: %v",
vmID.String(), nodeName, maxRetries, lastErr)
}
ln.log.Debug("verifyVMsAvailable: VM verified available",
"vmID", vmID.String(),
"nodeName", nodeName,
)
}
}
ln.log.Info("verifyVMsAvailable: all VMs verified available on all nodes",
"numVMs", len(vmIDs),
)
return nil
}
@@ -1371,7 +1727,11 @@ func createBlockchainTxs(
vmName := chainSpec.VMName
vmID, err := utils.VMID(vmName)
if err != nil {
return nil, err
log.Error("createBlockchainTxs: invalid VM name",
"error", err.Error(),
"vmName", vmName,
)
return nil, fmt.Errorf("invalid VM name %s: %w", vmName, err)
}
genesisBytes := chainSpec.Genesis
@@ -1387,11 +1747,17 @@ func createBlockchainTxs(
"vm-ID", vmID.String(),
"bytes length of genesis", len(genesisBytes),
)
ctx, cancel := createDefaultCtx(ctx)
defer cancel()
// Use different variable name to avoid shadowing outer context
txCtx, txCancel := createDefaultCtx(ctx)
chainID, err := ids.FromString(*chainSpec.ChainID)
if err != nil {
return nil, err
txCancel()
log.Error("createBlockchainTxs: invalid chain ID",
"error", err.Error(),
"chainID", *chainSpec.ChainID,
"vmName", vmName,
)
return nil, fmt.Errorf("invalid chain ID %s for VM %s: %w", *chainSpec.ChainID, vmName, err)
}
tx, err := w.pWallet.IssueCreateChainTx(
chainID,
@@ -1399,11 +1765,20 @@ func createBlockchainTxs(
vmID,
nil,
blockchainName,
common.WithContext(ctx),
common.WithContext(txCtx),
defaultPoll,
)
txCancel()
if err != nil {
return nil, fmt.Errorf("failure creating blockchain tx: %w", err)
log.Error("createBlockchainTxs: failed to issue create chain transaction",
"error", err.Error(),
"vmName", vmName,
"vmID", vmID.String(),
"chainID", chainID.String(),
"blockchainName", blockchainName,
"genesisLength", len(genesisBytes),
)
return nil, fmt.Errorf("failure creating blockchain tx for VM %s (chainID=%s): %w", vmName, chainID.String(), err)
}
blockchainTxs[i] = tx
+1 -1
View File
@@ -8,7 +8,7 @@
{
"version": "v1.9.6",
"old_name": "whitelisted-subnets",
"new_name": "track-subnets",
"new_name": "track-chains",
"value_map": ""
}
]
+161 -2
View File
@@ -9,16 +9,19 @@ import (
"encoding/hex"
"encoding/json"
"fmt"
"os"
"path/filepath"
"time"
"github.com/luxfi/constants"
"github.com/luxfi/crypto/bls/signer/localsigner"
"github.com/luxfi/genesis/configs"
"github.com/luxfi/ids"
"github.com/luxfi/keys"
"github.com/luxfi/netrunner/network"
"github.com/luxfi/netrunner/network/node"
"github.com/luxfi/node/config"
"github.com/luxfi/node/staking"
"github.com/luxfi/constants"
"github.com/luxfi/crypto/bls/signer/localsigner"
"github.com/luxfi/node/utils/formatting/address"
"github.com/luxfi/node/vms/platformvm/signer"
"golang.org/x/exp/maps"
@@ -325,3 +328,159 @@ func NewConfigForNetworkWithCustomGenesis(binaryPath string, numNodes uint32, ge
return netConfig, nil
}
// NewConfigWithPreExistingKeys creates a network config using pre-existing validator keys.
// This is useful for:
// - Maintaining consistent NodeIDs across network restarts
// - Using keys with pre-configured BLS signers
// - Deploying to mainnet/testnet with known validator identities
//
// The keysDir should contain subdirectories (e.g., node1, node2) with:
// - staker.crt and staker.key for TLS identity
// - bls/signer.key for BLS signer (optional)
// - ec/private.key for P-Chain addresses (optional)
func NewConfigWithPreExistingKeys(binaryPath string, networkID uint32, keysDir string) (network.Config, error) {
// Get genesis for the specified network
genesisJSON, err := configs.GetGenesis(networkID)
if err != nil {
return network.Config{}, fmt.Errorf("failed to get genesis for network %d: %w", networkID, err)
}
// Load validator keys from the keys directory
ks := keys.NewKeyStore(keysDir)
validatorKeys, err := ks.LoadAll()
if err != nil {
return network.Config{}, fmt.Errorf("failed to load validator keys from %s: %w", keysDir, err)
}
if len(validatorKeys) == 0 {
return network.Config{}, fmt.Errorf("no validator keys found in %s", keysDir)
}
// Start with the default config structure
netConfig := NewDefaultConfig(binaryPath)
// Parse genesis to modify it
var genesis map[string]interface{}
if err := json.Unmarshal(genesisJSON, &genesis); err != nil {
return network.Config{}, fmt.Errorf("failed to parse genesis: %w", err)
}
// Build initial stakers from loaded keys
hrp := constants.GetHRP(networkID)
numNodes := uint32(len(validatorKeys))
initialStakers := make([]map[string]interface{}, numNodes)
for i, vk := range validatorKeys {
rewardAddr, err := address.Format("P", hrp, vk.PChainAddr[:])
if err != nil {
return network.Config{}, fmt.Errorf("couldn't format reward address for node %d: %w", i, err)
}
staker := map[string]interface{}{
"nodeID": vk.NodeID.String(),
"rewardAddress": rewardAddr,
"delegationFee": 20000, // 2% delegation fee
}
// Add BLS signer if available
if len(vk.BLSPublicKey) > 0 && len(vk.BLSPoP) > 0 {
staker["signer"] = map[string]interface{}{
"publicKey": vk.BLSPublicKeyHex(),
"proofOfPossession": vk.BLSPoPHex(),
}
}
initialStakers[i] = staker
}
// Update genesis with initial stakers
genesis["initialStakers"] = initialStakers
// Generate P-Chain allocations from the loaded keys
allocBuilder := keys.NewAllocationBuilder(networkID, validatorKeys).
WithAmount(100 * keys.MegaLux). // 100M LUX per validator
WithFeeAccount(0, 10*keys.MegaLux). // First validator gets extra for fees
WithImmediateUnlock()
keyAllocations, err := allocBuilder.Build()
if err != nil {
return network.Config{}, fmt.Errorf("failed to build allocations: %w", err)
}
// Convert P-Chain allocations to genesis format
pchainAllocs := make([]interface{}, len(keyAllocations.PChainAllocations))
for i, alloc := range keyAllocations.PChainAllocations {
unlockSchedule := make([]map[string]interface{}, len(alloc.UnlockSchedule))
for j, unlock := range alloc.UnlockSchedule {
unlockSchedule[j] = map[string]interface{}{
"amount": unlock.Amount,
"locktime": unlock.Locktime,
}
}
pchainAllocs[i] = map[string]interface{}{
"ethAddr": alloc.ETHAddr,
"luxAddr": alloc.LUXAddr,
"initialAmount": alloc.InitialAmount,
"unlockSchedule": unlockSchedule,
}
}
genesis["allocations"] = pchainAllocs
// Set initial staked funds
genesis["initialStakedFunds"] = keyAllocations.InitialStakedFunds
// Update start time to now
now := time.Now().Unix()
genesis["startTime"] = uint64(now)
// Re-serialize genesis
updatedGenesis, err := json.MarshalIndent(genesis, "", " ")
if err != nil {
return network.Config{}, fmt.Errorf("failed to serialize updated genesis: %w", err)
}
netConfig.Genesis = string(updatedGenesis)
// Configure node configs with the loaded staking keys
netConfig.NodeConfigs = make([]node.Config, numNodes)
for i, vk := range validatorKeys {
port := 9630 + int(i)*2
netConfig.NodeConfigs[i] = node.Config{
Flags: map[string]interface{}{
config.HTTPPortKey: port,
config.StakingPortKey: port + 1,
},
StakingKey: string(vk.StakerKey),
StakingCert: string(vk.StakerCert),
StakingSigningKey: base64.StdEncoding.EncodeToString(vk.BLSSecretKey),
IsBeacon: true,
ChainConfigFiles: map[string]string{},
UpgradeConfigFiles: map[string]string{},
PChainConfigFiles: map[string]string{},
}
}
return netConfig, nil
}
// DefaultKeysPath returns the default path for pre-existing validator keys
func DefaultKeysPath() string {
home, _ := os.UserHomeDir()
return filepath.Join(home, "work", "lux", "keys")
}
// NewMainnetConfigWithKeys creates a mainnet config using pre-existing validator keys
func NewMainnetConfigWithKeys(binaryPath string, keysDir string) (network.Config, error) {
if keysDir == "" {
keysDir = DefaultKeysPath()
}
return NewConfigWithPreExistingKeys(binaryPath, configs.LuxMainnetID, keysDir)
}
// NewTestnetConfigWithKeys creates a testnet config using pre-existing validator keys
func NewTestnetConfigWithKeys(binaryPath string, keysDir string) (network.Config, error) {
if keysDir == "" {
keysDir = DefaultKeysPath()
}
return NewConfigWithPreExistingKeys(binaryPath, configs.LuxTestnetID, keysDir)
}
+50 -159
View File
@@ -1,78 +1,72 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// SPDX-License-Identifier: BSD-3-Clause
// Package local provides network configuration for local development and testing.
// For key management, use github.com/luxfi/keys package directly.
package local
import (
"crypto/rand"
"encoding/hex"
"fmt"
"os"
"path/filepath"
"strings"
luxcrypto "github.com/luxfi/crypto/secp256k1"
ethcrypto "github.com/luxfi/crypto"
luxcrypto "github.com/luxfi/crypto/secp256k1"
"github.com/luxfi/ids"
"github.com/luxfi/keys"
"github.com/luxfi/node/utils/formatting/address"
)
// KeyInfo contains computed addresses from a private key
// KeyInfo contains computed addresses from a private key.
// Deprecated: Use github.com/luxfi/keys.ValidatorKey instead.
type KeyInfo struct {
PrivKeyHex string
EthAddr string
ShortID ids.ShortID
}
// DefaultKeyPath returns the default path for lux keys
// DefaultKeyPath returns the default path for lux keys.
// Deprecated: Use keys.NewKeyStore("").
func DefaultKeyPath() string {
home, _ := os.UserHomeDir()
return filepath.Join(home, ".lux", "keys")
return keys.NewKeyStore("").BaseDir()
}
// LoadOrGenerateKeys loads validator keys from the specified path or generates new ones if missing.
// Keys are stored as hex-encoded private keys in files named validator_XXX.pk
// LoadOrGenerateKeys loads validator keys using the keys package.
// Deprecated: Use keys.KeyStore.LoadAll() or keys.KeyStore.GenerateMultiple().
func LoadOrGenerateKeys(keyPath string, count int) ([]KeyInfo, error) {
if keyPath == "" {
keyPath = DefaultKeyPath()
ks := keys.NewKeyStore(keyPath)
// Try to load existing keys first
vkeys, err := ks.LoadAll()
if err != nil || len(vkeys) == 0 {
// Generate new keys
vkeys, err = ks.GenerateMultiple(count, "validator")
if err != nil {
return nil, fmt.Errorf("failed to generate keys: %w", err)
}
}
// Ensure key directory exists
if err := os.MkdirAll(keyPath, 0700); err != nil {
return nil, fmt.Errorf("failed to create key directory: %w", err)
// Ensure we have enough keys
if len(vkeys) < count {
// Generate additional keys
additional, err := ks.GenerateMultiple(count-len(vkeys), "validator")
if err != nil {
return nil, fmt.Errorf("failed to generate additional keys: %w", err)
}
vkeys = append(vkeys, additional...)
}
keys := make([]KeyInfo, count)
// Convert to KeyInfo for backward compatibility
result := make([]KeyInfo, count)
for i := 0; i < count; i++ {
keyFile := filepath.Join(keyPath, fmt.Sprintf("validator_%03d.pk", i))
var privKeyHex string
data, err := os.ReadFile(keyFile)
if err != nil {
// Key doesn't exist, generate new one
privKey, genErr := generatePrivateKey()
if genErr != nil {
return nil, fmt.Errorf("failed to generate key %d: %w", i, genErr)
}
privKeyHex = hex.EncodeToString(privKey)
// Save the new key
if err := os.WriteFile(keyFile, []byte(privKeyHex+"\n"), 0600); err != nil {
return nil, fmt.Errorf("failed to save key %d: %w", i, err)
}
} else {
privKeyHex = strings.TrimSpace(string(data))
vk := vkeys[i]
result[i] = KeyInfo{
PrivKeyHex: hex.EncodeToString(vk.ECPrivateKey),
EthAddr: vk.CChainAddrHex(),
ShortID: vk.PChainAddr,
}
// Compute addresses from the key
keyInfo, err := ComputeKeyInfo(privKeyHex)
if err != nil {
return nil, fmt.Errorf("failed to compute addresses for key %d: %w", i, err)
}
keys[i] = keyInfo
}
return keys, nil
return result, nil
}
// ComputeKeyInfo derives addresses from a hex-encoded private key
@@ -109,133 +103,30 @@ func FormatAddress(chainID string, hrp string, shortID ids.ShortID) (string, err
return address.Format(chainID, hrp, shortID[:])
}
// generatePrivateKey generates a new random 32-byte private key
func generatePrivateKey() ([]byte, error) {
key := make([]byte, 32)
if _, err := rand.Read(key); err != nil {
return nil, err
}
return key, nil
}
// UTXO chain constants (P/X chains)
//
// UTXO Base Unit: MicroLux (10^-6 LUX) - consensus constraint, uses uint64
// UTXO Decimals: 6
//
// EVM Base Unit: WeiLux (10^-18 LUX) - presentation only, uses uint256
// ERC20 Decimals: 18
// Re-export constants from keys package for backward compatibility
// Deprecated: Use github.com/luxfi/keys constants directly
const (
// MicroLux is the base unit for UTXO chains (P/X)
// 1 LUX = 1,000,000 MicroLux
MicroLux uint64 = 1_000_000 // 10^6
// OneBillionLUX is 1B LUX in MicroLux (UTXO base unit)
// 1,000,000,000 * 10^6 = 10^15 MicroLux
OneBillionLUX uint64 = 1_000_000_000 * MicroLux
// OneMillionLUX is 1M LUX in MicroLux (UTXO base unit)
// 1,000,000 * 10^6 = 10^12 MicroLux
OneMillionLUX uint64 = 1_000_000 * MicroLux
// OnePercentLUX is 1% of 1B = 10M LUX in MicroLux
OnePercentLUX uint64 = OneBillionLUX / 100
// SecondsPerYear for vesting calculations
SecondsPerYear uint64 = 365 * 24 * 3600
// Jan1_2020 is Unix timestamp for Jan 1, 2020 00:00:00 UTC (vesting start)
Jan1_2020 uint64 = 1577836800
MicroLux = keys.MicroLux
Lux = keys.Lux
MegaLux = keys.MegaLux
GigaLux = keys.GigaLux
OneMillionLUX = keys.MegaLux
OneBillionLUX = keys.GigaLux
DefaultValidatorStake = keys.DefaultValidatorStake
)
// ImmediateUnlockLUX is 5% of 1B = 50M LUX for immediate spending (fees, transactions)
const ImmediateUnlockLUX uint64 = OneBillionLUX * 5 / 100
// DefaultValidatorStake is 1M LUX per validator in μLUX
const DefaultValidatorStake uint64 = OneMillionLUX
// GenerateVestingSchedule creates an unlock schedule with:
// - 5% immediately available (locktime=0) for transaction fees
// - 1% per year for 95 years starting from Jan 1, 2020
// This ensures the wallet has spendable funds for chain creation and other operations.
func GenerateVestingSchedule() []map[string]interface{} {
// First entry: immediately available funds (locktime=0)
schedule := make([]map[string]interface{}, 96) // 1 immediate + 95 vested
schedule[0] = map[string]interface{}{
"amount": ImmediateUnlockLUX,
"locktime": uint64(0), // Immediately available
}
// Remaining 95% vests 1% per year starting from 2020
for year := 0; year < 95; year++ {
unlockTime := Jan1_2020 + (uint64(year) * SecondsPerYear)
schedule[year+1] = map[string]interface{}{
"amount": OnePercentLUX,
"locktime": unlockTime,
}
}
return schedule
}
// GenerateAllocationsFromKeys creates genesis allocations for loaded keys with vesting
// The first key gets immediately spendable funds (locktime=0) for transaction fees.
// Other keys get vesting schedules starting from Jan 1, 2020.
func GenerateAllocationsFromKeys(keys []KeyInfo, hrp string) ([]map[string]interface{}, error) {
allocations := make([]map[string]interface{}, len(keys))
for i, key := range keys {
luxAddr, err := FormatAddress("P", hrp, key.ShortID)
if err != nil {
return nil, fmt.Errorf("failed to format address for key %d: %w", i, err)
}
var unlockSchedule []map[string]interface{}
if i == 0 {
// First key: all funds immediately available (locktime=0) for transactions
// This matches how mainnet treasury works
unlockSchedule = []map[string]interface{}{
{
"amount": OneBillionLUX,
"locktime": uint64(0),
},
}
} else {
// Other keys: use vesting schedule
unlockSchedule = GenerateVestingSchedule()
}
allocations[i] = map[string]interface{}{
"ethAddr": key.EthAddr,
"luxAddr": luxAddr,
"initialAmount": uint64(0), // initialAmount is NOT immediately spendable
"unlockSchedule": unlockSchedule,
}
}
return allocations, nil
}
// GenerateCChainAllocFromKeys creates C-chain genesis allocations for loaded keys
func GenerateCChainAllocFromKeys(keys []KeyInfo) map[string]map[string]string {
alloc := make(map[string]map[string]string)
balanceHex := fmt.Sprintf("0x%x", OneBillionLUX)
for _, key := range keys {
alloc[key.EthAddr] = map[string]string{"balance": balanceHex}
}
return alloc
}
// GenerateValidatorAllocations creates P-Chain allocations for a given number of validators.
// Each validator gets DefaultValidatorStake (1M LUX) with funds immediately available.
// The first validator gets extra funds for transaction fees and chain creation.
// Deprecated: Use keys.NewAllocationBuilder directly.
func GenerateValidatorAllocations(numValidators uint32, hrp string) ([]map[string]interface{}, []KeyInfo, error) {
// Generate or load keys for the validators
keys, err := LoadOrGenerateKeys("", int(numValidators))
keyInfos, err := LoadOrGenerateKeys("", int(numValidators))
if err != nil {
return nil, nil, fmt.Errorf("failed to generate validator keys: %w", err)
}
allocations := make([]map[string]interface{}, numValidators)
for i := uint32(0); i < numValidators; i++ {
key := keys[i]
key := keyInfos[i]
luxAddr, err := FormatAddress("P", hrp, key.ShortID)
if err != nil {
return nil, nil, fmt.Errorf("failed to format address for validator %d: %w", i, err)
@@ -262,5 +153,5 @@ func GenerateValidatorAllocations(numValidators uint32, hrp string) ([]map[strin
}
}
return allocations, keys, nil
return allocations, keyInfos, nil
}
+39 -1
View File
@@ -8,6 +8,7 @@ import (
"errors"
"fmt"
"io/fs"
"net"
"net/netip"
"os"
"os/user"
@@ -944,7 +945,7 @@ func (ln *localNetwork) restartNode(
}
if trackChains != "" {
nodeConfig.Flags[config.TrackNetsKey] = trackChains
nodeConfig.Flags[config.TrackChainsKey] = trackChains
}
// keep same ports, dbdir in node flags
@@ -967,10 +968,19 @@ func (ln *localNetwork) restartNode(
}
if !node.paused {
// Get the ports before removing the node
apiPort := node.GetAPIPort()
p2pPort := node.GetP2PPort()
if err := ln.removeNode(ctx, nodeName); err != nil {
return err
}
syscall.Sync()
// Wait for ports to be released (TCP TIME_WAIT)
// This prevents "bind: address already in use" errors
// Use shorter timeout (5s) since ports are usually available quickly after process stop
waitForPortsAvailable(apiPort, p2pPort, 5*time.Second)
}
if _, err := ln.addNode(nodeConfig); err != nil {
@@ -997,6 +1007,34 @@ func (ln *localNetwork) isPausedNode(nodeConfig *node.Config) bool {
return false
}
// waitForPortsAvailable waits until the specified ports are available for binding.
// This is necessary after stopping a node to ensure the ports are released from TIME_WAIT state.
func waitForPortsAvailable(apiPort, p2pPort uint16, timeout time.Duration) {
deadline := time.Now().Add(timeout)
checkInterval := 100 * time.Millisecond
for time.Now().Before(deadline) {
apiAvailable := isPortAvailable(apiPort)
p2pAvailable := isPortAvailable(p2pPort)
if apiAvailable && p2pAvailable {
return
}
time.Sleep(checkInterval)
}
}
// isPortAvailable checks if a TCP port is available for binding
func isPortAvailable(port uint16) bool {
ln, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", port))
if err != nil {
return false
}
ln.Close()
return true
}
// Set [nodeConfig].Name if it isn't given and assert it's unique.
func (ln *localNetwork) setNodeName(nodeConfig *node.Config) error {
// If no name was given, use default name pattern
+1 -1
View File
@@ -130,7 +130,7 @@ func (node *localNode) AttachPeer(ctx context.Context, router router.InboundHand
Network: peer.TestNetwork,
Router: router,
VersionCompatibility: version.GetCompatibility(time.Now()),
MyNets: set.Set[ids.ID]{},
MyChains: set.Set[ids.ID]{},
Beacons: validators.NewManager(),
Validators: validators.NewManager(),
NetworkID: node.networkID,
+1 -1
View File
@@ -41,7 +41,7 @@ func fixDeprecatedLuxdFlags(flags map[string]interface{}) error {
return fmt.Errorf("expected %q to be of type string but got %T", deprecatedWhitelistedChainsKey, vIntf)
}
if v != "" {
flags[config.TrackNetsKey] = v
flags[config.TrackChainsKey] = v
}
delete(flags, deprecatedWhitelistedChainsKey)
}
+2 -2
View File
@@ -232,7 +232,7 @@ func (lc *localNetwork) createConfig() error {
}
if lc.options.trackChains != "" {
cfg.NodeConfigs[i].Flags[config.TrackNetsKey] = lc.options.trackChains
cfg.NodeConfigs[i].Flags[config.TrackChainsKey] = lc.options.trackChains
}
cfg.NodeConfigs[i].BinaryPath = lc.execPath
@@ -725,7 +725,7 @@ func (lc *localNetwork) updateNodeInfo() error {
lc.nodeInfos = make(map[string]*rpcpb.NodeInfo)
for name, node := range nodes {
trackChains, err := node.GetFlag(config.TrackNetsKey)
trackChains, err := node.GetFlag(config.TrackChainsKey)
if err != nil {
return err
}
+56 -11
View File
@@ -518,20 +518,37 @@ func (s *server) CreateBlockchains(
defer s.mu.Unlock()
if s.network == nil {
s.log.Error("CreateBlockchains: network not bootstrapped")
return nil, ErrNotBootstrapped
}
s.log.Debug("CreateBlockchains")
// Log the incoming request for debugging
s.log.Info("CreateBlockchains: received request",
zap.Int("numBlockchainSpecs", len(req.GetBlockchainSpecs())),
)
if len(req.GetBlockchainSpecs()) == 0 {
s.log.Error("CreateBlockchains: no blockchain specs provided")
return nil, ErrNoChainSpec
}
// Log details of each blockchain spec being processed
chainSpecs := []network.ChainSpec{}
for _, spec := range req.GetBlockchainSpecs() {
for i, spec := range req.GetBlockchainSpecs() {
s.log.Info("CreateBlockchains: processing blockchain spec",
zap.Int("index", i),
zap.String("vmName", spec.GetVmName()),
zap.Bool("hasGenesis", spec.GetGenesis() != ""),
zap.Bool("hasChainId", spec.GetChainId() != ""),
)
chainSpec, err := getNetworkChainSpec(s.log, spec, false, s.network.pluginDir)
if err != nil {
return nil, err
s.log.Error("CreateBlockchains: failed to parse blockchain spec",
zap.Error(err),
zap.Int("specIndex", i),
zap.String("vmName", spec.GetVmName()),
)
return nil, fmt.Errorf("failed to parse blockchain spec %d (VM=%s): %w", i, spec.GetVmName(), err)
}
chainSpecs = append(chainSpecs, chainSpec)
}
@@ -543,6 +560,11 @@ func (s *server) CreateBlockchains(
for _, chainSpec := range chainSpecs {
if chainSpec.ChainID != nil && !chainsSet.Contains(*chainSpec.ChainID) {
s.log.Error("CreateBlockchains: chain ID does not exist",
zap.String("chainID", *chainSpec.ChainID),
zap.String("vmName", chainSpec.VMName),
zap.Strings("existingChains", chainIDsList),
)
return nil, fmt.Errorf("chain id %q does not exist", *chainSpec.ChainID)
}
}
@@ -550,18 +572,39 @@ func (s *server) CreateBlockchains(
s.clusterInfo.Healthy = false
s.clusterInfo.CustomChainsHealthy = false
s.log.Info("CreateBlockchains: starting chain creation",
zap.Int("numChains", len(chainSpecs)),
zap.Duration("timeout", waitForHealthyTimeout),
)
ctx, cancel := context.WithTimeout(context.Background(), waitForHealthyTimeout)
defer cancel()
chainIDs, err := s.network.CreateChains(ctx, chainSpecs)
if err != nil {
s.log.Error("failed to create blockchains", zap.Error(err), zap.String("errorDetail", err.Error()))
fmt.Printf("ERROR: failed to create blockchains: %v\n", err)
s.stopAndRemoveNetwork(err)
return nil, err
} else {
s.updateClusterInfo()
// Build detailed error context for logging
vmNames := make([]string, len(chainSpecs))
for i, spec := range chainSpecs {
vmNames[i] = spec.VMName
}
s.log.Error("CreateBlockchains: failed to create blockchains",
zap.Error(err),
zap.String("errorDetail", fmt.Sprintf("%+v", err)),
zap.Strings("vmNames", vmNames),
zap.Int("numChainSpecs", len(chainSpecs)),
zap.String("pluginDir", s.network.pluginDir),
)
// Also print to stdout for immediate visibility
fmt.Printf("ERROR: CreateBlockchains failed: %v\n", err)
fmt.Printf("ERROR: VMs attempted: %v\n", vmNames)
fmt.Printf("ERROR: Plugin directory: %s\n", s.network.pluginDir)
// Don't stop the entire network on chain creation failure - keep it running
// so user can retry or investigate. This makes the network more resilient.
return nil, fmt.Errorf("CreateBlockchains failed for VMs %v: %w", vmNames, err)
}
s.log.Info("custom chains created")
s.updateClusterInfo()
s.log.Info("CreateBlockchains: custom chains created successfully",
zap.Int("numChains", len(chainIDs)),
)
strChainIDs := []string{}
for _, chainID := range chainIDs {
@@ -788,7 +831,9 @@ func (s *server) CreateChains(_ context.Context, req *rpcpb.CreateChainsRequest)
chainIDs, err := s.network.CreateParticipantGroups(ctx, participantsSpecs)
if err != nil {
s.log.Error("failed to create chains", zap.Error(err))
s.stopAndRemoveNetwork(err)
// Don't stop the entire network on chain creation failure - keep it running
// so user can retry or investigate. This makes the network more resilient.
// s.stopAndRemoveNetwork(err) // Commented out for resilience
return nil, err
} else {
s.updateClusterInfo()
+15 -15
View File
@@ -54,7 +54,7 @@ var (
gRPCGatewayEp string
execPath1 string
execPath2 string
subnetEvmPath string
evmPath string
genesisPath string
genesisContents string
@@ -147,10 +147,10 @@ func init() {
"node executable path (to upgrade to)",
)
flag.StringVar(
&subnetEvmPath,
"subnet-evm-path",
&evmPath,
"evm-path",
"",
"path to subnet-evm binary",
"path to evm binary",
)
}
@@ -187,7 +187,7 @@ var _ = ginkgo.BeforeSuite(func() {
}, log)
gomega.Ω(err).Should(gomega.BeNil())
genesisPath = "tests/e2e/subnet-evm-genesis.json"
genesisPath = "tests/e2e/evm-genesis.json"
genesisByteContents, err := os.ReadFile(genesisPath)
gomega.Ω(err).Should(gomega.BeNil())
genesisContents = string(genesisByteContents)
@@ -217,7 +217,7 @@ var _ = ginkgo.Describe("[Start/Remove/Restart/Add/Stop]", func() {
client.WithPluginDir(filepath.Join(filepath.Dir(execPath1), "plugins")),
client.WithChainSpecs([]*rpcpb.BlockchainSpec{
{
VmName: "subnetevm",
VmName: "evm",
Genesis: genesisPath, // test genesis path usage
},
}),
@@ -235,7 +235,7 @@ var _ = ginkgo.Describe("[Start/Remove/Restart/Add/Stop]", func() {
resp, err := cli.CreateChains(ctx,
[]*rpcpb.BlockchainSpec{
{
VmName: "subnetevm",
VmName: "evm",
Genesis: genesisPath, // test genesis path usage
},
},
@@ -275,7 +275,7 @@ var _ = ginkgo.Describe("[Start/Remove/Restart/Add/Stop]", func() {
resp, err := cli.CreateChains(ctx,
[]*rpcpb.BlockchainSpec{
{
VmName: "subnetevm",
VmName: "evm",
Genesis: genesisContents,
SubnetId: &existingSubnetID,
},
@@ -315,7 +315,7 @@ var _ = ginkgo.Describe("[Start/Remove/Restart/Add/Stop]", func() {
_, err := cli.CreateChains(ctx,
[]*rpcpb.BlockchainSpec{
{
VmName: "subnetevm",
VmName: "evm",
Genesis: genesisContents,
SubnetId: &existingSubnetID,
},
@@ -331,7 +331,7 @@ var _ = ginkgo.Describe("[Start/Remove/Restart/Add/Stop]", func() {
resp, err := cli.CreateChains(ctx,
[]*rpcpb.BlockchainSpec{
{
VmName: "subnetevm",
VmName: "evm",
Genesis: genesisContents,
ParticipantsSpec: &rpcpb.ParticipantsSpec{Participants: subnetParticipants},
},
@@ -363,7 +363,7 @@ var _ = ginkgo.Describe("[Start/Remove/Restart/Add/Stop]", func() {
resp, err := cli.CreateChains(ctx,
[]*rpcpb.BlockchainSpec{
{
VmName: "subnetevm",
VmName: "evm",
Genesis: genesisContents,
ParticipantsSpec: &rpcpb.ParticipantsSpec{Participants: subnetParticipants2},
},
@@ -395,12 +395,12 @@ var _ = ginkgo.Describe("[Start/Remove/Restart/Add/Stop]", func() {
resp, err := cli.CreateChains(ctx,
[]*rpcpb.BlockchainSpec{
{
VmName: "subnetevm",
VmName: "evm",
Genesis: genesisContents,
SubnetId: &existingSubnetID,
},
{
VmName: "subnetevm",
VmName: "evm",
Genesis: genesisContents,
SubnetId: &existingSubnetID,
},
@@ -422,12 +422,12 @@ var _ = ginkgo.Describe("[Start/Remove/Restart/Add/Stop]", func() {
resp, err := cli.CreateChains(ctx,
[]*rpcpb.BlockchainSpec{
{
VmName: "subnetevm",
VmName: "evm",
Genesis: genesisContents,
ParticipantsSpec: &rpcpb.ParticipantsSpec{Participants: disjointNewSubnetParticipants[0]},
},
{
VmName: "subnetevm",
VmName: "evm",
Genesis: genesisContents,
ParticipantsSpec: &rpcpb.ParticipantsSpec{Participants: disjointNewSubnetParticipants[1]},
},
+6 -6
View File
@@ -23,21 +23,21 @@ func TestSetJSONKey(t *testing.T) {
"log-level":"INFO",
"log-dir":"INFO",
"db-dir":"INFO",
"track-subnets":"a,b,c",
"track-chains":"a,b,c",
"plugin-dir":"INFO"
}`
s, err := SetJSONKey(b, "track-subnets", "d,e,f")
s, err := SetJSONKey(b, "track-chains", "d,e,f")
require.NoError(t, err)
require.Contains(t, s, `"track-subnets":"d,e,f"`)
require.Contains(t, s, `"track-chains":"d,e,f"`)
// now check it's actual correct JSON
var m map[string]interface{}
err = json.Unmarshal([]byte(s), &m)
require.NoError(t, err)
// check if one-liner also works
bb := `{"api-admin-enabled":true,"api-ipcs-enabled":true,"db-dir":"/tmp/network-runner-root-data3856302950/node5/db-dir","health-check-frequency":"2s","index-enabled":true,"log-dir":"/tmp/network-runner-root-data3856302950/node5/log","log-display-level":"INFO","log-level":"INFO","network-max-reconnect-delay":"1s","network-peer-list-gossip-frequency":"250ms","plugin-dir":"/home/fabio/go/src/github.com/luxfi/node/build/plugins","public-ip":"127.0.0.1","track-subnets":""}`
ss, err := SetJSONKey(bb, "track-subnets", "d,e,f")
bb := `{"api-admin-enabled":true,"api-ipcs-enabled":true,"db-dir":"/tmp/network-runner-root-data3856302950/node5/db-dir","health-check-frequency":"2s","index-enabled":true,"log-dir":"/tmp/network-runner-root-data3856302950/node5/log","log-display-level":"INFO","log-level":"INFO","network-max-reconnect-delay":"1s","network-peer-list-gossip-frequency":"250ms","plugin-dir":"/home/fabio/go/src/github.com/luxfi/node/build/plugins","public-ip":"127.0.0.1","track-chains":""}`
ss, err := SetJSONKey(bb, "track-chains", "d,e,f")
require.NoError(t, err)
require.Contains(t, s, `"track-subnets":"d,e,f"`)
require.Contains(t, s, `"track-chains":"d,e,f"`)
// also check here it's correct JSON
err = json.Unmarshal([]byte(ss), &m)
require.NoError(t, err)