mirror of
https://github.com/luxfi/netrunner.git
synced 2026-07-27 00:04:23 +00:00
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:
@@ -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
@@ -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
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
{
|
||||
"version": "v1.9.6",
|
||||
"old_name": "whitelisted-subnets",
|
||||
"new_name": "track-subnets",
|
||||
"new_name": "track-chains",
|
||||
"value_map": ""
|
||||
}
|
||||
]
|
||||
|
||||
+161
-2
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user