Update logging, examples

This commit is contained in:
Zach Kelling
2025-12-22 05:07:52 -08:00
parent 40e2bbf737
commit 35f02edd25
10 changed files with 846 additions and 237 deletions
+174 -19
View File
@@ -9,12 +9,15 @@ import (
"encoding/hex"
"errors"
"fmt"
"io"
"net/http"
"os"
"sort"
"strings"
"time"
"github.com/luxfi/genesis/pkg/genesis"
"github.com/luxfi/keys"
"github.com/luxfi/node/vms/platformvm/reward"
"github.com/luxfi/node/vms/components/lux"
@@ -97,7 +100,7 @@ func (ln *localNetwork) getNode() node.Node {
// get node client URI for an arbitrary node in the network
func (ln *localNetwork) getClientURI() (string, error) { //nolint
node := ln.getNode()
clientURI := fmt.Sprintf("http://%s:%d", node.GetURL(), node.GetAPIPort())
clientURI := node.GetURL() // GetURL now returns full URL http://host:port
ln.log.Info("getClientURI",
"nodeName", node.GetName(),
"uri", clientURI)
@@ -233,7 +236,7 @@ func (ln *localNetwork) waitForChainsDiscoveredOnAllNodes(
fmt.Println()
ln.log.Info(luxlog.Blue.Wrap(luxlog.Bold.Wrap("waiting for chains to be discovered on all nodes...")))
maxWait := 60 * time.Second
maxWait := 2 * time.Minute
pollInterval := 2 * time.Second
deadline := time.Now().Add(maxWait)
@@ -250,9 +253,11 @@ func (ln *localNetwork) waitForChainsDiscoveredOnAllNodes(
discovered := false
for time.Now().Before(deadline) {
// Try to query the chain RPC endpoint - if it responds, the chain is discovered
// Try to query the chain RPC endpoint with eth_chainId - 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)
// Use a proper JSON-RPC request body to ensure the EVM endpoint responds
jsonRPCBody := strings.NewReader(`{"jsonrpc":"2.0","method":"eth_chainId","params":[],"id":1}`)
req, err := http.NewRequestWithContext(ctx, "POST", chainRPCURL, jsonRPCBody)
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
@@ -261,21 +266,34 @@ func (ln *localNetwork) waitForChainsDiscoveredOnAllNodes(
client := &http.Client{Timeout: 5 * time.Second}
resp, err := client.Do(req)
if err == nil {
body, _ := io.ReadAll(resp.Body)
resp.Body.Close()
// Any response (even error) means the chain endpoint exists
discovered = true
ln.log.Debug("chain discovered on node",
// Check if we got a valid response (not a 404 or other error)
if resp.StatusCode == 200 {
discovered = true
ln.log.Debug("chain discovered on node",
"node", nodeName,
"blockchain-id", blockchainID,
"status", resp.StatusCode,
"response", string(body),
)
break
}
ln.log.Debug("chain endpoint returned non-200",
"node", nodeName,
"blockchain-id", blockchainID,
"status", resp.StatusCode,
"response", string(body),
)
} else {
ln.log.Debug("chain not yet discovered, waiting",
"node", nodeName,
"blockchain-id", blockchainID,
"error", err.Error(),
"url", chainRPCURL,
)
break
}
ln.log.Debug("chain not yet discovered, waiting",
"node", nodeName,
"blockchain-id", blockchainID,
)
select {
case <-ctx.Done():
return ctx.Err()
@@ -446,6 +464,15 @@ func (ln *localNetwork) installCustomChains(
return nil, fmt.Errorf("failed to add primary validators: %w", err)
}
// Ensure P-chain has liquid funds for chain creation
// Genesis allocations go to X-chain, so we may need to export/import to P-chain
if err := fundPChainFromXChain(ctx, w, ln.log); err != nil {
ln.log.Error("installCustomChains: failed to fund P-chain from X-chain",
"error", err.Error(),
)
return nil, fmt.Errorf("failed to fund P-chain from X-chain: %w", err)
}
// create missing chains
chainIDs, err := createChains(ctx, uint32(len(participantsSpecs)), w, ln.log)
if err != nil {
@@ -864,17 +891,58 @@ type wallet struct {
}
// getDefaultKey loads the first key from ~/.lux/keys for wallet operations.
// Keys are loaded from disk, never hardcoded in source.
// Priority: LUX_MNEMONIC > LUX_PRIVATE_KEY > disk keys
func getDefaultKey() (*secp256k1.PrivateKey, error) {
keys, err := LoadOrGenerateKeys("", 1)
// If LUX_MNEMONIC is set, derive key from mnemonic (index 0)
if mnemonic := os.Getenv("LUX_MNEMONIC"); mnemonic != "" {
fmt.Printf("🔑 getDefaultKey: Using LUX_MNEMONIC (len=%d)\n", len(mnemonic))
vk, err := keys.DeriveValidatorFromMnemonic(mnemonic, 0)
if err != nil {
return nil, fmt.Errorf("failed to derive key from mnemonic: %w", err)
}
fmt.Printf("🔑 VK.PChainAddr (from mnemonic derivation): %s\n", vk.PChainAddr.String())
privKey, err := secp256k1.ToPrivateKey(vk.ECPrivateKey)
if err != nil {
return nil, err
}
pubKey := privKey.PublicKey()
walletAddr := ids.ShortID(pubKey.Address())
fmt.Printf("🔑 Wallet address (from secp256k1 key): %s\n", walletAddr.String())
fmt.Printf("🔑 Addresses match: %v\n", vk.PChainAddr == walletAddr)
return privKey, nil
}
// If LUX_PRIVATE_KEY is set, use it directly (hex encoded, 64 chars = 32 bytes)
if privKeyHex := os.Getenv("LUX_PRIVATE_KEY"); privKeyHex != "" {
fmt.Printf("🔑 getDefaultKey: Using LUX_PRIVATE_KEY (len=%d): %s...\n", len(privKeyHex), privKeyHex[:16])
privKeyBytes, err := hex.DecodeString(privKeyHex)
if err != nil {
return nil, fmt.Errorf("failed to decode LUX_PRIVATE_KEY: %w", err)
}
privKey, err := secp256k1.ToPrivateKey(privKeyBytes)
if err != nil {
return nil, err
}
pubKey := privKey.PublicKey()
addr := ids.ShortID(pubKey.Address())
fmt.Printf("🔑 getDefaultKey: Derived address ShortID: %s\n", addr.String())
return privKey, nil
}
// Fall back to loading from disk
fmt.Printf("🔑 getDefaultKey: Falling back to disk keys\n")
loadedKeys, err := LoadOrGenerateKeys("", 1)
if err != nil {
return nil, fmt.Errorf("failed to load keys from ~/.lux/keys: %w", err)
}
if len(keys) == 0 {
if len(loadedKeys) == 0 {
return nil, errors.New("no keys found in ~/.lux/keys")
}
fmt.Printf("🔑 getDefaultKey: Loaded key from disk: %s...\n", loadedKeys[0].PrivKeyHex[:16])
// Convert hex to private key
privKeyBytes, err := hex.DecodeString(keys[0].PrivKeyHex)
privKeyBytes, err := hex.DecodeString(loadedKeys[0].PrivKeyHex)
if err != nil {
return nil, fmt.Errorf("invalid key format: %w", err)
}
@@ -892,6 +960,10 @@ func newWallet(
return nil, fmt.Errorf("failed to get wallet key: %w", err)
}
kc := secp256k1fx.NewKeychain(privKey)
// Debug: print addresses being queried
fmt.Printf("🔍 Wallet querying for addresses: %v\n", kc.Addrs)
// Use dedicated timeout context for FetchState to avoid parent context cancellation propagation
fetchCtx, fetchCancel := createDefaultCtx(ctx)
luxState, err := primary.FetchState(fetchCtx, uri, kc.Addrs)
@@ -899,6 +971,16 @@ func newWallet(
if err != nil {
return nil, fmt.Errorf("failed to fetch state from %s: %w", uri, err)
}
// Debug: print network context and UTXOs
xChainIDForDebug := luxState.XCTX.BlockchainID
fmt.Printf("🔍 Network ID: %d, X-chain ID: %s\n", luxState.XCTX.NetworkID, xChainIDForDebug)
fmt.Printf("🔍 LUX Asset ID: %s\n", luxState.XCTX.XAssetID)
xUtxos, _ := luxState.UTXOs.UTXOs(ctx, xChainIDForDebug, xChainIDForDebug)
fmt.Printf("🔍 Fetched %d X-chain UTXOs\n", len(xUtxos))
for i, utxo := range xUtxos {
fmt.Printf(" UTXO[%d]: ID=%s, AssetID=%s\n", i, utxo.UTXOID.TxID, utxo.AssetID())
}
pClient := platformvm.NewClient(uri)
pTXs := make(map[ids.ID]*txs.Tx)
for _, id := range preloadTXs {
@@ -967,8 +1049,14 @@ func (ln *localNetwork) addPrimaryValidators(
for _, v := range vdrs {
curValidators.Add(v.NodeID)
}
// Debug: log current validators from genesis
fmt.Printf("🔍 GetCurrentValidators returned %d validators:\n", len(vdrs))
for _, v := range vdrs {
fmt.Printf(" - %s (weight: %d)\n", v.NodeID.String(), v.Weight)
}
for nodeName, node := range ln.nodes {
nodeID := node.GetNodeID()
fmt.Printf("🔍 Checking node %s (ID: %s) - in curValidators: %v\n", nodeName, nodeID.String(), curValidators.Contains(nodeID))
if curValidators.Contains(nodeID) {
continue
@@ -1092,6 +1180,73 @@ func importPChainFromXChain(ctx context.Context, w *wallet, owner *secp256k1fx.O
return err
}
// fundPChainFromXChain ensures the wallet has liquid P-chain funds for chain creation.
// With direct P-chain genesis allocations, this may not need to do anything.
// Falls back to X->P export/import if P-chain has insufficient funds.
func fundPChainFromXChain(ctx context.Context, w *wallet, log luxlog.Logger) error {
// Amount needed for chain operations (1 LUX should be plenty for fees)
const requiredAmount = uint64(1_000_000_000) // 1 LUX in nLUX
// Debug: print wallet address for verification against genesis
fmt.Printf("🔍 DEBUG: Wallet address (short): %s\n", w.addr.String())
fmt.Printf("🔍 DEBUG: Wallet P-chain address: P-lux1%s (bech32 would differ)\n", w.addr.String())
fmt.Printf("🔍 DEBUG: LUX asset ID: %s\n", w.luxAssetID.String())
// First check if P-chain already has sufficient funds
balances, err := w.pBuilder.GetBalance()
if err != nil {
log.Warn("failed to check P-chain balance, will try X->P transfer", "error", err.Error())
fmt.Printf("⚠️ DEBUG: P-chain balance check failed: %v\n", err)
} else {
fmt.Printf("🔍 DEBUG: P-chain balances map has %d entries\n", len(balances))
for assetID, bal := range balances {
fmt.Printf(" - Asset %s: %d nLUX\n", assetID.String(), bal)
}
pBalance := balances[w.luxAssetID]
log.Info("P-chain balance check", "balance", pBalance, "required", requiredAmount)
fmt.Printf("💰 P-chain balance: %d nLUX (need %d)\n", pBalance, requiredAmount)
if pBalance >= requiredAmount {
log.Info(luxlog.Green.Wrap("P-chain already has sufficient funds, skipping X->P transfer"))
fmt.Printf("✅ P-chain already funded, no transfer needed\n")
return nil
}
}
// Also check X-chain balance for debugging
xBalances, xErr := w.xWallet.Builder().GetFTBalance()
if xErr != nil {
fmt.Printf("⚠️ DEBUG: X-chain balance check failed: %v\n", xErr)
} else {
fmt.Printf("🔍 DEBUG: X-chain balances:\n")
for assetID, bal := range xBalances {
fmt.Printf(" - Asset %s: %d nLUX\n", assetID.String(), bal)
}
}
// P-chain doesn't have enough, try to export from X-chain
owner := &secp256k1fx.OutputOwners{
Threshold: 1,
Addrs: []ids.ShortID{w.addr},
}
log.Info(luxlog.Green.Wrap("funding P-chain from X-chain for chain creation"))
fmt.Printf("💰 Exporting %d nLUX from X-chain to P-chain for address %s\n", requiredAmount, w.addr.String())
// Export LUX from X-chain to P-chain
if err := exportXChainToPChain(ctx, w, owner, w.luxAssetID, requiredAmount); err != nil {
return fmt.Errorf("failed to export from X-chain: %w", err)
}
fmt.Printf("✅ Export from X-chain completed\n")
// Import on P-chain
if err := importPChainFromXChain(ctx, w, owner, w.xChainID); err != nil {
return fmt.Errorf("failed to import on P-chain: %w", err)
}
fmt.Printf("✅ Import on P-chain completed, wallet funded\n")
return nil
}
func (ln *localNetwork) removeChainValidators(
ctx context.Context,
removeParticipantsSpecs []network.RemoveChainValidatorSpec,
@@ -1591,7 +1746,7 @@ func (ln *localNetwork) reloadVMPlugins(ctx context.Context) error {
if node.paused {
continue
}
uri := fmt.Sprintf("http://%s:%d", node.GetURL(), node.GetAPIPort())
uri := node.GetURL()
adminCli := admin.NewClient(uri)
// Use different variable name to avoid shadowing outer context
apiCtx, cancel := createDefaultCtx(ctx)
@@ -1654,7 +1809,7 @@ func (ln *localNetwork) verifyVMsAvailable(ctx context.Context, vmIDs []ids.ID)
if node.paused {
continue
}
uri := fmt.Sprintf("http://%s:%d", node.GetURL(), node.GetAPIPort())
uri := node.GetURL()
adminCli := admin.NewClient(uri)
for _, vmID := range vmIDs {
+313 -141
View File
@@ -4,7 +4,6 @@
package local
import (
"crypto/tls"
"encoding/base64"
"encoding/hex"
"encoding/json"
@@ -15,6 +14,7 @@ import (
"github.com/luxfi/constants"
"github.com/luxfi/crypto/bls/signer/localsigner"
luxcrypto "github.com/luxfi/crypto/secp256k1"
"github.com/luxfi/genesis/configs"
"github.com/luxfi/ids"
"github.com/luxfi/keys"
@@ -23,18 +23,17 @@ import (
"github.com/luxfi/node/config"
"github.com/luxfi/node/staking"
"github.com/luxfi/node/utils/formatting/address"
"github.com/luxfi/node/vms/platformvm/signer"
"golang.org/x/exp/maps"
)
// NewConfigForNetwork creates a network config for the specified network ID.
// This uses the proper genesis configuration from github.com/luxfi/genesis/configs.
// For non-local networks (mainnet/testnet), it dynamically injects initial stakers
// based on the generated node staking keys.
//
// When LUX_MNEMONIC environment variable is set, validator keys are derived
// deterministically from the mnemonic, ensuring consistent node identities
// and P-Chain allocations across network restarts.
// For mainnet/testnet, this function:
// 1. Uses embedded validator keys from defaultNetworkConfig (not generating new ones)
// 2. Preserves initialStakers from the genesis package (already matches embedded keys)
// 3. Generates P-chain allocations for the embedded validators (10M LUX each)
// 4. Preserves cchainGenesis from the genesis package
//
// Supported network IDs:
// - 96369: LUX Mainnet
@@ -47,7 +46,7 @@ func NewConfigForNetwork(binaryPath string, numNodes uint32, networkID uint32) (
return network.Config{}, fmt.Errorf("failed to get genesis for network %d: %w", networkID, err)
}
// Start with the default config structure
// Start with the default config structure (includes embedded validator keys)
netConfig := NewDefaultConfig(binaryPath)
// For local network (1337), use the genesis as-is since it already has stakers
@@ -96,8 +95,10 @@ func NewConfigForNetwork(binaryPath string, numNodes uint32, networkID uint32) (
return netConfig, nil
}
// For mainnet/testnet, we need to generate new staking keys and inject them
// as initial stakers in the genesis
// For mainnet/testnet:
// - Use embedded validator keys from defaultNetworkConfig
// - Keep initialStakers from genesis (already matches embedded keys)
// - Generate P-chain allocations for the embedded validators
// Parse genesis to modify it
var genesis map[string]interface{}
@@ -105,136 +106,112 @@ func NewConfigForNetwork(binaryPath string, numNodes uint32, networkID uint32) (
return network.Config{}, fmt.Errorf("failed to parse genesis: %w", err)
}
// Generate staking keys for all nodes and collect staker info
type stakerInfo struct {
nodeID ids.NodeID
stakingKey string
stakingCrt string
signerKey string
pop *signer.ProofOfPossession
}
stakers := make([]stakerInfo, numNodes)
for i := uint32(0); i < numNodes; i++ {
// Generate new staking cert/key
stakingCert, stakingKey, err := staking.NewCertAndKeyBytes()
if err != nil {
return network.Config{}, fmt.Errorf("couldn't generate staking Cert/Key for node %d: %w", i, err)
}
// Parse the cert to get NodeID
tlsCert, err := tls.X509KeyPair(stakingCert, stakingKey)
if err != nil {
return network.Config{}, fmt.Errorf("couldn't parse TLS cert for node %d: %w", i, err)
}
// Convert to ids.Certificate for NodeID computation
if len(tlsCert.Certificate) == 0 {
return network.Config{}, fmt.Errorf("no certificate data for node %d", i)
}
idsCert := &ids.Certificate{
Raw: tlsCert.Certificate[0],
PublicKey: tlsCert.PrivateKey,
}
nodeID := ids.NodeIDFromCert(idsCert)
// Generate BLS signer key and proof of possession
blsKey, err := localsigner.New()
if err != nil {
return network.Config{}, fmt.Errorf("couldn't generate BLS key for node %d: %w", i, err)
}
pop, err := signer.NewProofOfPossession(blsKey)
if err != nil {
return network.Config{}, fmt.Errorf("couldn't generate proof of possession for node %d: %w", i, err)
}
stakers[i] = stakerInfo{
nodeID: nodeID,
stakingKey: string(stakingKey),
stakingCrt: string(stakingCert),
signerKey: base64.StdEncoding.EncodeToString(blsKey.ToBytes()),
pop: pop,
}
}
// Create initial stakers for genesis
hrp := constants.GetHRP(networkID)
// Treasury address short ID (derived from 0x9011E888251AB053B7bD1cdB598Db4f9DEd94714)
// P-Chain Address: P-lux1c7wevm4667l4umtzh93r25wpxlpsadkhka6gv6
var treasuryShortID ids.ShortID
treasuryBytes, _ := hex.DecodeString("c79d966ebad7bf5e6d62b9623551c137c30eb6d7")
copy(treasuryShortID[:], treasuryBytes)
rewardAddr, err := address.Format("P", hrp, treasuryShortID[:])
if err != nil {
return network.Config{}, fmt.Errorf("couldn't format reward address: %w", err)
// Number of embedded validators available
numEmbedded := uint32(len(defaultNetworkConfig.NodeConfigs))
if numEmbedded > 5 {
numEmbedded = 5 // Cap at 5 embedded validators
}
initialStakers := make([]map[string]interface{}, numNodes)
for i, s := range stakers {
initialStakers[i] = map[string]interface{}{
"nodeID": s.nodeID.String(),
"rewardAddress": rewardAddr,
"delegationFee": 20000, // 2% delegation fee
"signer": map[string]interface{}{
"publicKey": "0x" + hex.EncodeToString(s.pop.PublicKey[:]),
"proofOfPossession": "0x" + hex.EncodeToString(s.pop.ProofOfPossession[:]),
},
// Use min(numNodes, numEmbedded) for node configs with embedded keys
numEmbeddedToUse := numNodes
if numEmbeddedToUse > numEmbedded {
numEmbeddedToUse = numEmbedded
}
// For P/X-chain allocations, we need to create them for keys the wallet can use.
// C-chain allocations are historic (embedded in genesis), but P/X can be set freely.
// REPLACE allocations with ones for the wallet's key (mnemonic or private key).
var newAllocations []interface{}
// If LUX_MNEMONIC is set, create allocations for mnemonic-derived key
// Need BOTH X-chain (for transfers) and P-chain (for chain creation/validators)
if mnemonic := os.Getenv("LUX_MNEMONIC"); mnemonic != "" {
validatorKeys, err := keys.DeriveValidatorsFromMnemonic(mnemonic, 1)
if err == nil && len(validatorKeys) > 0 {
vk := validatorKeys[0]
addr := vk.PChainAddr // Same underlying secp256k1 address
xLuxAddr, errX := address.Format("X", hrp, addr[:])
pLuxAddr, errP := address.Format("P", hrp, addr[:])
if errX == nil && errP == nil {
fmt.Printf("🔑 Setting X+P allocations for LUX_MNEMONIC: X=%s, P=%s (2B each)\n", xLuxAddr, pLuxAddr)
// X-chain allocation (for asset transfers)
newAllocations = append(newAllocations, map[string]interface{}{
"ethAddr": vk.CChainAddrHex(),
"luxAddr": xLuxAddr,
"initialAmount": uint64(2000000000000000000), // 2B LUX
"unlockSchedule": []map[string]interface{}{},
})
// P-chain allocation (for chain creation, validators)
// IMPORTANT: Must have unlockSchedule entries for builder to create UTXOs
newAllocations = append(newAllocations, map[string]interface{}{
"ethAddr": vk.CChainAddrHex(),
"luxAddr": pLuxAddr,
"initialAmount": uint64(0), // Not used for P-chain, unlockSchedule is used
"unlockSchedule": []map[string]interface{}{
{
"amount": uint64(2000000000000000000), // 2B LUX
"locktime": uint64(0), // Immediately available
},
},
})
}
}
}
// Update genesis with initial stakers
genesis["initialStakers"] = initialStakers
// Check if genesis already has allocations from the configs package
// (which may include dynamic allocations from ~/.lux/genesis/{network}/pchain.json)
existingAllocs, hasAllocs := genesis["allocations"].([]interface{})
if hasAllocs && len(existingAllocs) > 0 {
// Use existing allocations from genesis - don't overwrite
// This preserves dynamic P-Chain allocations configured via:
// - LUX_PCHAIN_ALLOCS environment variable
// - LUX_PCHAIN_ALLOCS_FILE environment variable
// - ~/.lux/genesis/{network}/pchain.json file
} else {
// No allocations in genesis - auto-generate based on numNodes (--num-validators)
// Each validator gets 1M LUX (DefaultValidatorStake) immediately available
// First validator gets 10M LUX for fees, chain creation, etc.
validatorAllocs, _, err := GenerateValidatorAllocations(numNodes, hrp)
if err != nil {
return network.Config{}, fmt.Errorf("failed to generate validator allocations: %w", err)
}
// Convert to []interface{} for JSON marshaling
allocations := make([]interface{}, len(validatorAllocs))
for i, alloc := range validatorAllocs {
allocations[i] = alloc
}
genesis["allocations"] = allocations
}
// C-chain genesis is immutable - don't modify it
// The C-chain genesis from configs package contains the real mainnet state
now := time.Now().Unix()
// Check if initialStakedFunds already exists in genesis from configs package
// If not, extract from the second allocation's address
if _, hasStakedFunds := genesis["initialStakedFunds"]; !hasStakedFunds {
// Extract second allocation address for initialStakedFunds
if allocs, ok := genesis["allocations"].([]interface{}); ok && len(allocs) > 1 {
if secondAlloc, ok := allocs[1].(map[string]interface{}); ok {
if luxAddr, ok := secondAlloc["luxAddr"].(string); ok {
genesis["initialStakedFunds"] = []string{luxAddr}
// If LUX_PRIVATE_KEY is set, also create X+P allocations for it
if privKeyHex := os.Getenv("LUX_PRIVATE_KEY"); privKeyHex != "" {
privKeyBytes, err := hex.DecodeString(privKeyHex)
if err == nil && len(privKeyBytes) == 32 {
luxPrivKey, err := luxcrypto.ToPrivateKey(privKeyBytes)
if err == nil {
pubKey := luxPrivKey.PublicKey()
addr := ids.ShortID(pubKey.Address())
xLuxAddr, errX := address.Format("X", hrp, addr[:])
pLuxAddr, errP := address.Format("P", hrp, addr[:])
if errX == nil && errP == nil {
fmt.Printf("🔑 Adding X+P allocations for LUX_PRIVATE_KEY: X=%s, P=%s (2B each)\n", xLuxAddr, pLuxAddr)
ethAddr := "0x" + hex.EncodeToString(addr[:])
// X-chain allocation
newAllocations = append(newAllocations, map[string]interface{}{
"ethAddr": ethAddr,
"luxAddr": xLuxAddr,
"initialAmount": uint64(2000000000000000000), // 2B LUX
"unlockSchedule": []map[string]interface{}{},
})
// P-chain allocation - must use unlockSchedule for builder to create UTXOs
newAllocations = append(newAllocations, map[string]interface{}{
"ethAddr": ethAddr,
"luxAddr": pLuxAddr,
"initialAmount": uint64(0), // Not used for P-chain
"unlockSchedule": []map[string]interface{}{
{
"amount": uint64(2000000000000000000), // 2B LUX
"locktime": uint64(0), // Immediately available
},
},
})
}
}
}
// Fall back to empty if we couldn't extract
if _, hasStakedFunds := genesis["initialStakedFunds"]; !hasStakedFunds {
genesis["initialStakedFunds"] = []string{}
}
}
// If we created new allocations, replace the genesis allocations
// Otherwise, keep the genesis package's allocations (for backward compatibility)
if len(newAllocations) > 0 {
genesis["allocations"] = newAllocations
}
// Keep initialStakers from genesis package - they already match the embedded keys
// The genesis package's pchain.json has the correct NodeIDs and BLS signers
// DO NOT overwrite initialStakers
// Set initialStakedFunds to empty - allocations are for free balance, not staking
genesis["initialStakedFunds"] = []string{}
// Update start time to now
now := time.Now().Unix()
genesis["startTime"] = uint64(now)
// Re-serialize genesis
@@ -244,22 +221,52 @@ func NewConfigForNetwork(binaryPath string, numNodes uint32, networkID uint32) (
}
netConfig.Genesis = string(updatedGenesis)
// Configure node configs with the generated staking keys
// Configure node configs using embedded keys for the first numEmbeddedToUse nodes
netConfig.NodeConfigs = make([]node.Config, numNodes)
for i := uint32(0); i < numNodes; i++ {
port := 9630 + int(i)*2
netConfig.NodeConfigs[i] = node.Config{
Flags: map[string]interface{}{
config.HTTPPortKey: port,
config.StakingPortKey: port + 1,
},
StakingKey: stakers[i].stakingKey,
StakingCert: stakers[i].stakingCrt,
StakingSigningKey: stakers[i].signerKey,
IsBeacon: true,
ChainConfigFiles: map[string]string{},
UpgradeConfigFiles: map[string]string{},
PChainConfigFiles: map[string]string{},
if i < numEmbeddedToUse {
// Use embedded validator keys
embeddedConfig := defaultNetworkConfig.NodeConfigs[i]
netConfig.NodeConfigs[i] = node.Config{
Flags: map[string]interface{}{
config.HTTPPortKey: port,
config.StakingPortKey: port + 1,
},
StakingKey: embeddedConfig.StakingKey,
StakingCert: embeddedConfig.StakingCert,
StakingSigningKey: embeddedConfig.StakingSigningKey,
IsBeacon: true,
ChainConfigFiles: map[string]string{},
UpgradeConfigFiles: map[string]string{},
PChainConfigFiles: map[string]string{},
}
} else {
// Generate new keys for additional nodes beyond the 5 embedded ones
stakingCert, stakingKey, err := staking.NewCertAndKeyBytes()
if err != nil {
return network.Config{}, fmt.Errorf("couldn't generate staking Cert/Key for node %d: %w", i, err)
}
blsKey, err := localsigner.New()
if err != nil {
return network.Config{}, fmt.Errorf("couldn't generate BLS key for node %d: %w", i, err)
}
netConfig.NodeConfigs[i] = node.Config{
Flags: map[string]interface{}{
config.HTTPPortKey: port,
config.StakingPortKey: port + 1,
},
StakingKey: string(stakingKey),
StakingCert: string(stakingCert),
StakingSigningKey: base64.StdEncoding.EncodeToString(blsKey.ToBytes()),
IsBeacon: true,
ChainConfigFiles: map[string]string{},
UpgradeConfigFiles: map[string]string{},
PChainConfigFiles: map[string]string{},
}
}
}
@@ -380,7 +387,8 @@ func NewConfigWithPreExistingKeys(binaryPath string, networkID uint32, keysDir s
staker := map[string]interface{}{
"nodeID": vk.NodeID.String(),
"rewardAddress": rewardAddr,
"delegationFee": 20000, // 2% delegation fee
"delegationFee": 20000, // 2% delegation fee
"weight": GigaLux, // 1,000,000,000 LUX (1B) per validator
}
// Add BLS signer if available
@@ -506,3 +514,167 @@ func NewTestnetConfigWithKeys(binaryPath string, keysDir string) (network.Config
}
return NewConfigWithPreExistingKeys(binaryPath, configs.LuxTestnetID, keysDir)
}
// NewConfigFromMnemonic creates a network config by deriving validator keys from LUX_MNEMONIC.
// This is the preferred method for starting mainnet/testnet as it doesn't depend on files on disk.
// The mnemonic is used to derive 5 validator EC keys (for P-chain allocations).
// TLS staking certs and BLS keys are generated fresh for each run.
func NewConfigFromMnemonic(binaryPath string, networkID uint32, numNodes uint32) (network.Config, error) {
mnemonic := os.Getenv("LUX_MNEMONIC")
if mnemonic == "" {
return network.Config{}, fmt.Errorf("LUX_MNEMONIC environment variable not set")
}
fmt.Printf("🔑 Deriving %d validators from LUX_MNEMONIC...\n", numNodes)
// Derive validator keys from mnemonic
validatorKeys, err := keys.DeriveValidatorsFromMnemonic(mnemonic, int(numNodes))
if err != nil {
return network.Config{}, fmt.Errorf("failed to derive validator keys: %w", err)
}
// Get base genesis
genesisJSON, err := configs.GetGenesis(networkID)
if err != nil {
return network.Config{}, fmt.Errorf("failed to get genesis for network %d: %w", networkID, err)
}
// Start with default config
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)
}
hrp := constants.GetHRP(networkID)
// Build initial stakers from derived keys
initialStakers := make([]map[string]interface{}, numNodes)
// Each validator gets BOTH X-chain AND P-chain allocations (2 entries per validator)
allocations := make([]interface{}, 0, numNodes*2)
for i, vk := range validatorKeys {
fmt.Printf("🔍 Validator %d: PChainAddr ShortID = %s\n", i, vk.PChainAddr.String())
// Build P-chain address for staker rewards and P-chain operations
pChainAddr, err := address.Format("P", hrp, vk.PChainAddr[:])
if err != nil {
return network.Config{}, fmt.Errorf("failed to format P-chain address for validator %d: %w", i, err)
}
fmt.Printf("🔍 Validator %d: P-chain bech32 = %s\n", i, pChainAddr)
// Build X-chain address for asset transfers
xChainAddr, err := address.Format("X", hrp, vk.PChainAddr[:])
if err != nil {
return network.Config{}, fmt.Errorf("failed to format X-chain address for validator %d: %w", i, err)
}
fmt.Printf("🔍 Validator %d: X-chain bech32 = %s\n", i, xChainAddr)
// Initial staker entry
staker := map[string]interface{}{
"nodeID": vk.NodeID.String(),
"rewardAddress": xChainAddr, // Rewards go to X-chain address
"delegationFee": 20000, // 2% delegation fee
"weight": GigaLux, // 1B LUX per validator
}
if len(vk.BLSPublicKey) > 0 && len(vk.BLSPoP) > 0 {
staker["signer"] = map[string]interface{}{
"publicKey": vk.BLSPublicKeyHex(),
"proofOfPossession": vk.BLSPoPHex(),
}
}
initialStakers[i] = staker
// X-chain allocation (2B LUX) - for asset transfers
allocations = append(allocations, map[string]interface{}{
"ethAddr": vk.CChainAddrHex(),
"luxAddr": xChainAddr,
"initialAmount": uint64(2000000000000000000), // 2B LUX
"unlockSchedule": []map[string]interface{}{},
})
// P-chain allocation (2B LUX) - for chain creation, validators
// IMPORTANT: Must use unlockSchedule for builder to create UTXOs
allocations = append(allocations, map[string]interface{}{
"ethAddr": vk.CChainAddrHex(),
"luxAddr": pChainAddr,
"initialAmount": uint64(0), // Not used for P-chain
"unlockSchedule": []map[string]interface{}{
{
"amount": uint64(2000000000000000000), // 2B LUX
"locktime": uint64(0), // Immediately available
},
},
})
fmt.Printf(" Validator %d: %s -> X:%s P:%s (2B each)\n", i+1, vk.NodeID.String(), xChainAddr, pChainAddr)
}
genesis["initialStakers"] = initialStakers
genesis["allocations"] = allocations
// IMPORTANT: Set initialStakedFunds to EMPTY so that P-chain allocations
// are NOT filtered by the builder. The builder filters allocations where
// the underlying ShortID matches initialStakedFunds, which would incorrectly
// filter P-chain allocations that share the same address as X-chain validators.
// We use explicit weight in initialStakers instead of deriving from stakes.
genesis["initialStakedFunds"] = []string{}
// 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 genesis: %w", err)
}
netConfig.Genesis = string(updatedGenesis)
// Debug: write genesis to file
if err := os.WriteFile("/tmp/mnemonic_genesis.json", updatedGenesis, 0644); err != nil {
fmt.Printf("Warning: could not write debug genesis: %v\n", err)
} else {
fmt.Printf("📄 Genesis written to /tmp/mnemonic_genesis.json\n")
}
// Configure node configs with derived 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{},
}
}
fmt.Printf("✅ Network config ready with %d validators\n", numNodes)
return netConfig, nil
}
// NewMainnetConfigFromMnemonic creates mainnet config from LUX_MNEMONIC
func NewMainnetConfigFromMnemonic(binaryPath string, numNodes uint32) (network.Config, error) {
return NewConfigFromMnemonic(binaryPath, configs.LuxMainnetID, numNodes)
}
// NewTestnetConfigFromMnemonic creates testnet config from LUX_MNEMONIC
func NewTestnetConfigFromMnemonic(binaryPath string, numNodes uint32) (network.Config, error) {
return NewConfigFromMnemonic(binaryPath, configs.LuxTestnetID, numNodes)
}
// NewLocalConfigFromMnemonic creates local network config from LUX_MNEMONIC
// This uses network ID 1337 with "custom" HRP, which is simpler for testing
func NewLocalConfigFromMnemonic(binaryPath string, numNodes uint32) (network.Config, error) {
return NewConfigFromMnemonic(binaryPath, configs.LocalID, numNodes)
}
+8 -2
View File
@@ -13,7 +13,6 @@ import (
"github.com/luxfi/netrunner/network/node"
"github.com/luxfi/node/config"
"github.com/luxfi/constants"
luxlog "github.com/luxfi/log"
"go.uber.org/zap"
)
@@ -97,7 +96,9 @@ func writeFiles(networkID uint32, genesis []byte, nodeRootDir string, nodeConfig
contents: decodedStakingSigningKey,
},
}
if networkID != constants.LocalID {
// Always write genesis file if provided, even for LocalID
// This allows custom genesis (e.g., with mnemonic-derived validators) to override defaults
if len(genesis) > 0 {
files = append(files, file{
flagValue: filepath.Join(nodeRootDir, genesisFileName),
path: filepath.Join(nodeRootDir, genesisFileName),
@@ -191,12 +192,15 @@ func getPort(
switch gotPort := portIntf.(type) {
case int:
port = uint16(gotPort)
fmt.Printf("🔍 getPort: %s found in flags as int=%d\n", portKey, port)
case float64:
port = uint16(gotPort)
fmt.Printf("🔍 getPort: %s found in flags as float64=%d\n", portKey, port)
default:
return 0, fmt.Errorf("expected flag %q to be int/float64 but got %T", portKey, portIntf)
}
} else if portIntf, ok := configFile[portKey]; ok {
fmt.Printf("🔍 getPort: %s NOT in flags, checking configFile\n", portKey)
portFromConfigFile, ok := portIntf.(float64)
if !ok {
return 0, fmt.Errorf("expected flag %q to be float64 but got %T", portKey, portIntf)
@@ -205,12 +209,14 @@ func getPort(
} else {
// Use a random free port.
// Note: it is possible but unlikely for getFreePort to return the same port multiple times.
fmt.Printf("🔍 getPort: %s NOT in flags or configFile, using random port\n", portKey)
port, err = getFreePort()
if err != nil {
return 0, fmt.Errorf("couldn't get free port: %w", err)
}
}
if reassignIfUsed && isFreePort(port) != nil {
fmt.Printf("🔍 getPort: %s port %d NOT free, reassigning to random\n", portKey, port)
port, err = getFreePort()
if err != nil {
return 0, fmt.Errorf("couldn't get free port: %w", err)
+112 -4
View File
@@ -4,6 +4,7 @@ import (
"context"
"embed"
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
@@ -19,6 +20,12 @@ import (
"syscall"
"time"
"github.com/luxfi/crypto/bls"
luxcrypto "github.com/luxfi/crypto/secp256k1"
"github.com/luxfi/ids"
luxlog "github.com/luxfi/log"
"github.com/luxfi/math/set"
"github.com/luxfi/keys"
"github.com/luxfi/netrunner/api"
"github.com/luxfi/netrunner/network"
"github.com/luxfi/netrunner/network/node"
@@ -26,13 +33,10 @@ import (
"github.com/luxfi/netrunner/utils"
"github.com/luxfi/netrunner/utils/constants"
"github.com/luxfi/node/config"
"github.com/luxfi/ids"
"github.com/luxfi/node/network/peer"
"github.com/luxfi/node/staking"
"github.com/luxfi/node/utils/beacon"
"github.com/luxfi/crypto/bls"
luxlog "github.com/luxfi/log"
"github.com/luxfi/math/set"
"github.com/luxfi/node/utils/formatting/address"
"github.com/luxfi/node/utils/wrappers"
"go.uber.org/zap"
"golang.org/x/exp/maps"
@@ -182,6 +186,44 @@ func init() {
}
}
// If LUX_PRIVATE_KEY is set, also allocate funds to that address for chain creation
if privKeyHex := os.Getenv("LUX_PRIVATE_KEY"); privKeyHex != "" {
privKeyBytes, err := hex.DecodeString(privKeyHex)
if err == nil && len(privKeyBytes) == 32 {
// Derive P-chain address from private key
luxPrivKey, err := luxcrypto.ToPrivateKey(privKeyBytes)
if err == nil {
pubKey := luxPrivKey.PublicKey()
pChainAddr := ids.ShortID(pubKey.Address())
// For default network (local, ID 1337), use "custom" HRP
luxAddr, err := address.Format("X", "custom", pChainAddr[:])
if err == nil {
fmt.Printf("🔑 Adding default network allocation for LUX_PRIVATE_KEY address: %s\n", luxAddr)
privKeyAlloc := map[string]interface{}{
"ethAddr": "0x" + hex.EncodeToString(pChainAddr[:]),
"luxAddr": luxAddr,
"initialAmount": float64(0),
"unlockSchedule": []interface{}{
map[string]interface{}{
"amount": float64(100000000000000000), // 100M LUX in nLUX
"locktime": float64(0), // Immediately available
},
},
}
allocations = append(allocations, privKeyAlloc)
genesisMap["allocations"] = allocations
// CRITICAL: Also add to initialStakedFunds so funds are available on P-chain
// This is required for CreateChainTx and other P-chain operations
initialStakedFunds, _ := genesisMap["initialStakedFunds"].([]interface{})
initialStakedFunds = append(initialStakedFunds, luxAddr)
genesisMap["initialStakedFunds"] = initialStakedFunds
fmt.Printf("🔑 Added %s to initialStakedFunds for P-chain access\n", luxAddr)
}
}
}
}
// now we can marshal the *whole* thing into bytes
updatedGenesis, err := json.Marshal(genesisMap)
if err != nil {
@@ -415,6 +457,72 @@ func NewDefaultConfigNNodes(binaryPath string, numNodes uint32) (network.Config,
if int(numNodes) < len(netConfig.NodeConfigs) {
netConfig.NodeConfigs = netConfig.NodeConfigs[:numNodes]
}
// If LUX_MNEMONIC is set, add allocations for mnemonic-derived address
mnemonic := os.Getenv("LUX_MNEMONIC")
fmt.Printf("🔍 DEBUG: NewDefaultConfigNNodes called, LUX_MNEMONIC set=%v\n", mnemonic != "")
if mnemonic != "" {
vk, err := keys.DeriveValidatorFromMnemonic(mnemonic, 0)
if err != nil {
return netConfig, fmt.Errorf("failed to derive key from mnemonic: %w", err)
}
// Parse genesis
var genesis map[string]interface{}
if err := json.Unmarshal([]byte(netConfig.Genesis), &genesis); err != nil {
return netConfig, fmt.Errorf("failed to parse genesis: %w", err)
}
// Create X-chain and P-chain addresses
// Network ID 1337 uses "local" HRP (per constants.NetworkIDToHRP)
const hrp = "local"
fmt.Printf("🔍 DEBUG: vk.PChainAddr (base58): %s\n", vk.PChainAddr.String())
fmt.Printf("🔍 DEBUG: vk.PChainAddr (hex): %x\n", vk.PChainAddr[:])
xLuxAddr, err := address.Format("X", hrp, vk.PChainAddr[:])
if err != nil {
return netConfig, fmt.Errorf("failed to format X-chain address: %w", err)
}
pLuxAddr, err := address.Format("P", hrp, vk.PChainAddr[:])
if err != nil {
return netConfig, fmt.Errorf("failed to format P-chain address: %w", err)
}
fmt.Printf("🔑 Adding local network allocations for LUX_MNEMONIC: X=%s, P=%s (2B each)\n", xLuxAddr, pLuxAddr)
// Get existing allocations
allocations, _ := genesis["allocations"].([]interface{})
// Add X-chain allocation
allocations = append(allocations, map[string]interface{}{
"luxAddr": xLuxAddr,
"ethAddr": "0x0000000000000000000000000000000000000000",
"initialAmount": 2_000_000_000_000_000_000, // 2B LUX
"unlockSchedule": []interface{}{},
})
// Add P-chain allocation with unlockSchedule (builder only reads P-chain amounts from unlockSchedule)
allocations = append(allocations, map[string]interface{}{
"luxAddr": pLuxAddr,
"ethAddr": "0x0000000000000000000000000000000000000000",
"initialAmount": 0, // Not used for P-chain - amounts come from unlockSchedule
"unlockSchedule": []interface{}{
map[string]interface{}{
"amount": uint64(2_000_000_000_000_000_000), // 2B LUX
"locktime": uint64(0), // Immediately available
},
},
})
genesis["allocations"] = allocations
// Re-encode genesis
updatedGenesis, err := json.MarshalIndent(genesis, "", " ")
if err != nil {
return netConfig, fmt.Errorf("failed to encode updated genesis: %w", err)
}
netConfig.Genesis = string(updatedGenesis)
}
return netConfig, nil
}
+9 -2
View File
@@ -83,7 +83,7 @@ type localNode struct {
func defaultGetConnFunc(ctx context.Context, node node.Node) (net.Conn, error) {
dialer := net.Dialer{}
return dialer.DialContext(ctx, constants.NetworkType, net.JoinHostPort(node.GetURL(), fmt.Sprintf("%d", node.GetP2PPort())))
return dialer.DialContext(ctx, constants.NetworkType, net.JoinHostPort(node.GetHost(), fmt.Sprintf("%d", node.GetP2PPort())))
}
// AttachPeer: see Network
@@ -195,14 +195,21 @@ func (node *localNode) GetAPIClient() api.Client {
return node.client
}
// GetHost returns the node's host/IP (e.g. 127.0.0.1).
// See node.Node
func (node *localNode) GetURL() string {
func (node *localNode) GetHost() string {
if node.httpHost == "0.0.0.0" || node.httpHost == "." {
return "0.0.0.0"
}
return "127.0.0.1"
}
// GetURL returns the full HTTP API URL (e.g. http://127.0.0.1:9630).
// See node.Node
func (node *localNode) GetURL() string {
return fmt.Sprintf("http://%s:%d", node.GetHost(), node.apiPort)
}
// See node.Node
func (node *localNode) GetP2PPort() uint16 {
return node.p2pPort