Files
netrunner/local/genesis_config.go
T
Zach Kelling cd0dd2fccc fix: correct network ID usage and remove HID conflicts
- Remove ledger-lux-go transitive dependency
- Fix Network ID vs Chain ID confusion in genesis_config.go
- Use constants.MainnetID/TestnetID instead of configs.ChainID
- Fix port base calculation for multi-network mode
- Add snapshot server functionality
- Update protobuf definitions with new RPC methods
- Add mnemonic test coverage
2025-12-29 10:07:43 -08:00

1114 lines
43 KiB
Go

// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// SPDX-License-Identifier: BSD-3-Clause
package local
import (
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"time"
"github.com/luxfi/constants"
"github.com/luxfi/crypto/bls"
"github.com/luxfi/crypto/bls/signer/localsigner"
"github.com/luxfi/node/vms/platformvm/signer"
luxcrypto "github.com/luxfi/crypto/secp256k1"
"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/netrunner/utils"
"github.com/luxfi/node/config"
"github.com/luxfi/node/staking"
"github.com/luxfi/node/utils/formatting/address"
"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 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
// - 96368: LUX Testnet
// - 1337: Local development network
func NewConfigForNetwork(binaryPath string, numNodes uint32, networkID uint32) (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)
}
// 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
// matching the pre-generated node keys
// Note: configs.CustomID is 1337 for custom local development network
if networkID == configs.CustomID {
netConfig.Genesis = string(genesisJSON)
// Handle node count for local network
if int(numNodes) > len(netConfig.NodeConfigs) {
toAdd := int(numNodes) - len(netConfig.NodeConfigs)
refNodeConfig := netConfig.NodeConfigs[len(netConfig.NodeConfigs)-1]
refAPIPortIntf, ok := refNodeConfig.Flags[config.HTTPPortKey]
if !ok {
return netConfig, fmt.Errorf("could not get last standard api port from config")
}
refAPIPort, ok := refAPIPortIntf.(float64)
if !ok {
return netConfig, fmt.Errorf("expected float64 for last standard api port, got %T", refAPIPortIntf)
}
refStakingPortIntf, ok := refNodeConfig.Flags[config.StakingPortKey]
if !ok {
return netConfig, fmt.Errorf("could not get last standard staking port from config")
}
refStakingPort, ok := refStakingPortIntf.(float64)
if !ok {
return netConfig, fmt.Errorf("expected float64 for last standard staking port, got %T", refStakingPortIntf)
}
for i := 0; i < toAdd; i++ {
nodeConfig := refNodeConfig
stakingCert, stakingKey, err := staking.NewCertAndKeyBytes()
if err != nil {
return netConfig, fmt.Errorf("couldn't generate staking Cert/Key: %w", err)
}
nodeConfig.StakingKey = string(stakingKey)
nodeConfig.StakingCert = string(stakingCert)
nodeConfig.Flags = map[string]interface{}{
config.HTTPPortKey: int(refAPIPort) + (i+1)*2,
config.StakingPortKey: int(refStakingPort) + (i+1)*2,
}
netConfig.NodeConfigs = append(netConfig.NodeConfigs, nodeConfig)
}
}
if int(numNodes) < len(netConfig.NodeConfigs) {
netConfig.NodeConfigs = netConfig.NodeConfigs[:numNodes]
}
return netConfig, nil
}
// 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{}
if err := json.Unmarshal(genesisJSON, &genesis); err != nil {
return network.Config{}, fmt.Errorf("failed to parse genesis: %w", err)
}
hrp := constants.GetHRP(networkID)
// Number of embedded validators available
numEmbedded := uint32(len(defaultNetworkConfig.NodeConfigs))
if numEmbedded > 5 {
numEmbedded = 5 // Cap at 5 embedded validators
}
// 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
},
},
})
}
}
}
// 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
},
},
})
}
}
}
}
// 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
}
// Generate initialStakers from embedded validator keys if not provided in genesis
// This is needed because testnet/mainnet genesis may not have initialStakers defined
initialStakers, hasStakers := genesis["initialStakers"]
stakersSlice, isSlice := initialStakers.([]interface{})
if !hasStakers || !isSlice || len(stakersSlice) == 0 {
// Generate initialStakers from embedded validator keys
var generatedStakers []map[string]interface{}
for i := uint32(0); i < numEmbeddedToUse; i++ {
embeddedConfig := defaultNetworkConfig.NodeConfigs[i]
// Get NodeID from staking cert
nodeID, err := utils.ToNodeID([]byte(embeddedConfig.StakingKey), []byte(embeddedConfig.StakingCert))
if err != nil {
return network.Config{}, fmt.Errorf("failed to get NodeID for node %d: %w", i, err)
}
// Get BLS key and compute ProofOfPossession
blsKeyBytes, err := base64.StdEncoding.DecodeString(embeddedConfig.StakingSigningKey)
if err != nil {
return network.Config{}, fmt.Errorf("failed to decode BLS key for node %d: %w", i, err)
}
blsSecretKey, err := bls.SecretKeyFromBytes(blsKeyBytes)
if err != nil {
return network.Config{}, fmt.Errorf("failed to parse BLS key for node %d: %w", i, err)
}
pop, err := signer.NewProofOfPossession(blsSecretKey)
if err != nil {
return network.Config{}, fmt.Errorf("failed to create ProofOfPossession for node %d: %w", i, err)
}
// Create staker entry - use a reward address derived from the first key or a treasury address
// For simplicity, use the X-chain address from the first allocation or default
staker := map[string]interface{}{
"nodeID": nodeID.String(),
"rewardAddress": "P-test1yljhuvjkmtu0y5ls6kf4exsdd8gea9mp7faxl2", // Default testnet treasury
"delegationFee": uint32(20000), // 2%
"signer": map[string]interface{}{
"publicKey": fmt.Sprintf("0x%x", pop.PublicKey[:]),
"proofOfPossession": fmt.Sprintf("0x%x", pop.ProofOfPossession[:]),
},
}
generatedStakers = append(generatedStakers, staker)
}
genesis["initialStakers"] = generatedStakers
fmt.Printf("🔧 Generated %d initialStakers from embedded validator keys\n", len(generatedStakers))
}
// 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
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 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
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{},
}
}
}
return netConfig, nil
}
// NewMainnetConfig creates a network config for LUX Mainnet (network ID 96369)
func NewMainnetConfig(binaryPath string, numNodes uint32) (network.Config, error) {
return NewConfigForNetwork(binaryPath, numNodes, configs.MainnetChainID)
}
// NewTestnetConfig creates a network config for LUX Testnet (network ID 96368)
func NewTestnetConfig(binaryPath string, numNodes uint32) (network.Config, error) {
return NewConfigForNetwork(binaryPath, numNodes, configs.TestnetChainID)
}
// NewLocalConfig creates a network config for local development (network ID 1337)
// This is equivalent to NewDefaultConfigNNodes but uses the configs package.
func NewLocalConfig(binaryPath string, numNodes uint32) (network.Config, error) {
return NewConfigForNetwork(binaryPath, numNodes, configs.CustomID)
}
// NewCanonicalMainnetConfig creates a mainnet network config using the CANONICAL genesis bytes.
// This function loads the pre-serialized genesis.json file directly, ensuring byte-for-byte
// deterministic output. Use this to avoid "db contains invalid genesis hash" errors on restart.
//
// CRITICAL: For mainnet/testnet, always use this function or NewCanonicalTestnetConfig
// rather than functions that regenerate genesis (which causes non-deterministic bytes).
func NewCanonicalMainnetConfig(binaryPath string, numNodes uint32) (network.Config, error) {
return newCanonicalConfig(binaryPath, numNodes, configs.MainnetID, 9630)
}
// NewCanonicalTestnetConfig creates a testnet network config using the CANONICAL genesis bytes.
// See NewCanonicalMainnetConfig for details on why this is critical.
func NewCanonicalTestnetConfig(binaryPath string, numNodes uint32) (network.Config, error) {
return newCanonicalConfig(binaryPath, numNodes, configs.TestnetID, 9640)
}
// NewCanonicalDevnetConfig creates a devnet network config using the CANONICAL genesis bytes.
// See NewCanonicalMainnetConfig for details on why this is critical.
func NewCanonicalDevnetConfig(binaryPath string, numNodes uint32) (network.Config, error) {
return newCanonicalConfig(binaryPath, numNodes, configs.DevnetID, 9650)
}
// NewCanonicalCustomConfig creates a custom (local) network config using the CANONICAL genesis bytes.
// See NewCanonicalMainnetConfig for details on why this is critical.
func NewCanonicalCustomConfig(binaryPath string, numNodes uint32) (network.Config, error) {
return newCanonicalConfig(binaryPath, numNodes, configs.CustomID, 9660)
}
// newCanonicalConfig creates a network config with canonical (pre-serialized) genesis bytes
// and loads validator keys from ~/.lux/keys/node{0..n-1}/
func newCanonicalConfig(binaryPath string, numNodes uint32, networkID uint32, portBase int) (network.Config, error) {
// Load CANONICAL genesis bytes - no parsing/re-serialization
genesisBytes, err := configs.GetCanonicalGenesisBytes(networkID)
if err != nil {
return network.Config{}, fmt.Errorf("failed to load canonical genesis: %w", err)
}
// Start with default config
netConfig := NewDefaultConfig(binaryPath)
netConfig.Genesis = string(genesisBytes)
// Load validator keys from ~/.lux/keys/
keysDir := os.Getenv("LUX_KEYS_DIR")
if keysDir == "" {
keysDir = validatorKeysDir()
}
ks := keys.NewKeyStore(keysDir)
// Load keys for each node
nodeConfigs := make([]node.Config, numNodes)
for i := uint32(0); i < numNodes; i++ {
name := fmt.Sprintf("node%d", i)
vk, err := ks.Load(name)
if err != nil {
return network.Config{}, fmt.Errorf("failed to load validator key %s from %s: %w (run 'lux key generate' first)", name, keysDir, err)
}
port := portBase + int(i)*2
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(" Loaded validator %d: %s\n", i, vk.NodeID.String())
}
netConfig.NodeConfigs = nodeConfigs
fmt.Printf("✅ Loaded canonical genesis for network %d with %d validators\n", networkID, numNodes)
return netConfig, nil
}
// NewConfigForNetworkWithCustomGenesis creates a network config with a custom genesis string.
// Use this for networks not defined in the configs package or for testing.
func NewConfigForNetworkWithCustomGenesis(binaryPath string, numNodes uint32, genesisJSON string) (network.Config, error) {
netConfig := NewDefaultConfig(binaryPath)
netConfig.Genesis = genesisJSON
// Handle node count
if int(numNodes) > len(netConfig.NodeConfigs) {
toAdd := int(numNodes) - len(netConfig.NodeConfigs)
refNodeConfig := netConfig.NodeConfigs[len(netConfig.NodeConfigs)-1]
refAPIPortIntf, ok := refNodeConfig.Flags[config.HTTPPortKey]
if !ok {
return netConfig, fmt.Errorf("could not get last standard api port from config")
}
refAPIPort, ok := refAPIPortIntf.(float64)
if !ok {
return netConfig, fmt.Errorf("expected float64 for last standard api port, got %T", refAPIPortIntf)
}
refStakingPortIntf, ok := refNodeConfig.Flags[config.StakingPortKey]
if !ok {
return netConfig, fmt.Errorf("could not get last standard staking port from config")
}
refStakingPort, ok := refStakingPortIntf.(float64)
if !ok {
return netConfig, fmt.Errorf("expected float64 for last standard staking port, got %T", refStakingPortIntf)
}
for i := 0; i < toAdd; i++ {
nodeConfig := node.Config{}
nodeConfig.Flags = maps.Clone(refNodeConfig.Flags)
stakingCert, stakingKey, err := staking.NewCertAndKeyBytes()
if err != nil {
return netConfig, fmt.Errorf("couldn't generate staking Cert/Key: %w", err)
}
nodeConfig.StakingKey = string(stakingKey)
nodeConfig.StakingCert = string(stakingCert)
nodeConfig.Flags[config.HTTPPortKey] = int(refAPIPort) + (i+1)*2
nodeConfig.Flags[config.StakingPortKey] = int(refStakingPort) + (i+1)*2
netConfig.NodeConfigs = append(netConfig.NodeConfigs, nodeConfig)
}
}
if int(numNodes) < len(netConfig.NodeConfigs) {
netConfig.NodeConfigs = netConfig.NodeConfigs[:numNodes]
}
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) {
// Determine port base based on network ID
// Mainnet (1): 9630 base
// Testnet (2): 9640 base
// Devnet (3): 9650 base
portBase := 9630
switch networkID {
case constants.TestnetID: // 2
portBase = 9640
case constants.DevnetID: // 3
portBase = 9650
}
// 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
"weight": GigaLux, // 1,000,000,000 LUX (1B) per validator
}
// 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 (our validator NodeIDs)
genesis["initialStakers"] = initialStakers
// Generate allocations from validator keys (rather than expecting pre-existing allocations)
// Each validator gets both X-chain and P-chain allocations
// X-chain: 1B LUX immediately available
// P-chain: 1B LUX with 1% unlock per year for 100 years starting Jan 1 2020
allocations := make([]interface{}, 0, numNodes*2)
// Constants for P-chain unlock schedule
const (
oneGigaLux = uint64(1000000000000000000) // 1B LUX in nLUX
onePercentPerYear = oneGigaLux / 100 // 1% = 10M LUX
jan1_2020 = uint64(1577836800) // Unix timestamp
oneYear = uint64(31536000) // seconds
)
for i, vk := range validatorKeys {
// Build P-chain address
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)
}
// Build X-chain address
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)
}
// X-chain allocation (1B LUX) - immediately available
allocations = append(allocations, map[string]interface{}{
"ethAddr": vk.CChainAddrHex(),
"luxAddr": xChainAddr,
"initialAmount": oneGigaLux,
"unlockSchedule": []map[string]interface{}{},
})
// P-chain allocation (1B LUX) - 1% per year for 100 years
pchainUnlockSchedule := make([]map[string]interface{}, 100)
for year := 0; year < 100; year++ {
pchainUnlockSchedule[year] = map[string]interface{}{
"amount": onePercentPerYear,
"locktime": jan1_2020 + (oneYear * uint64(year)),
}
}
allocations = append(allocations, map[string]interface{}{
"ethAddr": vk.CChainAddrHex(),
"luxAddr": pChainAddr,
"initialAmount": uint64(0),
"unlockSchedule": pchainUnlockSchedule,
})
fmt.Printf(" Validator %d: %s -> X:%s P:%s (1B each)\n", i+1, vk.NodeID.String(), xChainAddr, pChainAddr)
}
genesis["allocations"] = allocations
// Set initialStakedFunds to EMPTY - we use explicit weight in initialStakers
genesis["initialStakedFunds"] = []string{}
// Check for LUX_MNEMONIC and add mnemonic-derived allocation with FREE funds
// This enables subnet creation without needing the hardcoded treasury key
// IMPORTANT: Add this AFTER setting initialStakedFunds so mnemonic funds are NOT staked
if mnemonic := os.Getenv("LUX_MNEMONIC"); mnemonic != "" {
fmt.Printf("🔑 LUX_MNEMONIC is set, adding mnemonic allocation to genesis\n")
mnemonicAlloc := map[string]interface{}{
"ethAddr": "0x0406d56943a38ad8398a738527f27e2cf01731a8",
"luxAddr": "P-lux1qsrd262r5w9dswv2wwzj0un79ncpwvdgkpqzqu",
"initialAmount": 0,
"unlockSchedule": []map[string]interface{}{
{
"amount": uint64(100000000000000), // 100,000 LUX
"locktime": uint64(0),
},
},
}
// Append mnemonic allocation directly to our allocations slice
fmt.Printf(" Adding mnemonic allocation (NOT staked) to %d existing allocations\n", len(allocations))
allocations = append(allocations, mnemonicAlloc)
genesis["allocations"] = allocations
} else {
fmt.Printf("⚠️ LUX_MNEMONIC not set, skipping mnemonic allocation\n")
}
// Check for LUX_PRIVATE_KEY and add allocations with correct HRP for this network
// This enables deploy operations when using a specific private key
if privKeyHex := os.Getenv("LUX_PRIVATE_KEY"); privKeyHex != "" {
privKeyBytes, err := hex.DecodeString(privKeyHex)
if err == nil && len(privKeyBytes) == 32 {
// Derive address from private key
luxPrivKey, err := luxcrypto.ToPrivateKey(privKeyBytes)
if err == nil {
pubKey := luxPrivKey.PublicKey()
pChainAddr := ids.ShortID(pubKey.Address())
// Format addresses with the correct HRP for this network
xLuxAddr, errX := address.Format("X", hrp, pChainAddr[:])
pLuxAddr, errP := address.Format("P", hrp, pChainAddr[:])
if errX == nil && errP == nil {
ethAddr := "0x" + hex.EncodeToString(pChainAddr[:])
fmt.Printf("🔑 Adding LUX_PRIVATE_KEY allocations: X=%s, P=%s (2B each)\n", xLuxAddr, pLuxAddr)
// X-chain allocation (2B LUX) - immediately available
allocations = append(allocations, map[string]interface{}{
"ethAddr": ethAddr,
"luxAddr": xLuxAddr,
"initialAmount": uint64(2000000000000000000), // 2B LUX
"unlockSchedule": []map[string]interface{}{},
})
// P-chain allocation (2B LUX) - immediately available for subnet creation
allocations = append(allocations, map[string]interface{}{
"ethAddr": ethAddr,
"luxAddr": pLuxAddr,
"initialAmount": uint64(0),
"unlockSchedule": []map[string]interface{}{
{
"amount": uint64(2000000000000000000), // 2B LUX
"locktime": uint64(0), // Immediately available
},
},
})
genesis["allocations"] = allocations
// Add to initialStakedFunds for P-chain access
initialStakedFunds, _ := genesis["initialStakedFunds"].([]string)
initialStakedFunds = append(initialStakedFunds, xLuxAddr)
genesis["initialStakedFunds"] = initialStakedFunds
fmt.Printf("🔑 Added %s to initialStakedFunds for P-chain access\n", xLuxAddr)
}
}
}
}
// 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)
// Debug: write genesis to file for inspection
if err := os.WriteFile("/tmp/debug_genesis.json", updatedGenesis, 0644); err != nil {
fmt.Printf("Warning: could not write debug genesis: %v\n", err)
} else {
fmt.Printf("📄 Genesis written to /tmp/debug_genesis.json\n")
}
// Configure node configs with the loaded staking keys
netConfig.NodeConfigs = make([]node.Config, numNodes)
for i, vk := range validatorKeys {
port := portBase + 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, ".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, constants.MainnetID, 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, constants.TestnetID, keysDir)
}
// validatorKeysDir returns the directory path for persisted validator keys
func validatorKeysDir() string {
home, _ := os.UserHomeDir()
return filepath.Join(home, ".lux", "keys")
}
// NewConfigFromMnemonic creates a network config by deriving validator keys from LUX_MNEMONIC.
// This is the preferred method for starting mainnet/testnet.
//
// IMPORTANT: Keys are derived from mnemonic ONCE and persisted to disk (~/.lux/netrunner-validators/).
// On subsequent runs, keys are loaded from disk to maintain stable NodeIDs.
// This follows Avalanche's pattern where identity = persistent staking keys.
//
// The mnemonic is used to derive:
// - EC keys (for P-chain allocations) - deterministic from mnemonic
// - TLS staking certs (for NodeID) - generated once, then persisted
// - BLS keys (for consensus) - deterministic from mnemonic
func NewConfigFromMnemonic(binaryPath string, networkID uint32, numNodes uint32) (network.Config, error) {
fmt.Println(">>> ENTERED NewConfigFromMnemonic <<<")
mnemonic := os.Getenv("LUX_MNEMONIC")
if mnemonic == "" {
return network.Config{}, fmt.Errorf("LUX_MNEMONIC environment variable not set")
}
// Check if persisted validator keys exist
keysDir := os.Getenv("LUX_KEYS_DIR")
if keysDir == "" {
keysDir = validatorKeysDir()
}
ks := keys.NewKeyStore(keysDir)
var validatorKeys []*keys.ValidatorKey
var err error
// Try to load existing keys in order: node0, node1, ..., node{n-1}
// Keys must have EC private key (not just TLS certs) to be considered valid
validatorKeys = make([]*keys.ValidatorKey, numNodes)
allExist := true
for i := uint32(0); i < numNodes; i++ {
name := fmt.Sprintf("node%d", i)
vk, err := ks.Load(name)
if err != nil || vk == nil {
allExist = false
break
}
// Verify EC key exists - LoadFromDir auto-generates TLS certs but not EC keys
if len(vk.ECPrivateKey) == 0 {
fmt.Printf(" %s: missing EC private key, will re-derive from mnemonic\n", name)
allExist = false
break
}
validatorKeys[i] = vk
}
if allExist {
fmt.Printf("🔴 DEBUG_MARKER: allExist=true\n")
fmt.Printf("🔑 Loading %d validators from %s (stable NodeIDs)...\n", numNodes, keysDir)
for i, vk := range validatorKeys {
fmt.Printf(" node%d: %s\n", i, vk.NodeID.String())
}
} else {
// No existing keys - derive from mnemonic and persist
fmt.Printf("🔑 Deriving %d validators from LUX_MNEMONIC (first run)...\n", numNodes)
validatorKeys, err = keys.DeriveValidatorsFromMnemonic(mnemonic, int(numNodes))
if err != nil {
return network.Config{}, fmt.Errorf("failed to derive validator keys: %w", err)
}
// Persist keys to disk for stable NodeIDs on future runs
fmt.Printf("📁 Persisting validator keys to %s...\n", keysDir)
for i, vk := range validatorKeys {
name := fmt.Sprintf("node%d", i)
if err := ks.Save(name, vk); err != nil {
return network.Config{}, fmt.Errorf("failed to save validator key %d: %w", i, err)
}
fmt.Printf(" Saved %s: NodeID=%s\n", name, vk.NodeID.String())
}
}
fmt.Println("🔍 DEBUG: About to derive wallet key from mnemonic...")
// CRITICAL: Always derive the wallet key from mnemonic and ensure it has allocations.
// The wallet (used by deploy, fundPChainFromXChain, etc.) uses keys.DeriveValidatorFromMnemonic(mnemonic, 0),
// which may differ from validators loaded from ~/.lux/keys.
walletKey, err := keys.DeriveValidatorFromMnemonic(mnemonic, 0)
if err != nil {
return network.Config{}, fmt.Errorf("failed to derive wallet key from mnemonic: %w", err)
}
// Check if wallet key is different from all validator keys
walletNeedsAllocation := true
for _, vk := range validatorKeys {
if vk.PChainAddr == walletKey.PChainAddr {
walletNeedsAllocation = false
fmt.Printf("🔑 Wallet key matches validator key (no extra allocation needed)\n")
break
}
}
if walletNeedsAllocation {
fmt.Printf("🔑 Wallet key differs from validators - adding allocation for %s\n", walletKey.PChainAddr.String())
}
// 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
// P-chain allocation - MUST use unlockSchedule for P-chain UTXOs
// The builder processes unlockSchedule for P-chain, initialAmount for X-chain
allocations = append(allocations, map[string]interface{}{
"ethAddr": vk.CChainAddrHex(),
"luxAddr": pChainAddr,
"initialAmount": uint64(0),
"unlockSchedule": []map[string]interface{}{
{
"amount": uint64(2000000000000000000), // 2B LUX
"locktime": uint64(0), // locktime 0 = immediately unlocked
},
},
})
fmt.Printf(" Validator %d: %s -> X:%s P:%s (2B each)\n", i+1, vk.NodeID.String(), xChainAddr, pChainAddr)
}
// Add wallet key allocation if different from validators
if walletNeedsAllocation {
// Build X-chain address for wallet
walletXAddr, err := address.Format("X", hrp, walletKey.PChainAddr[:])
if err != nil {
return network.Config{}, fmt.Errorf("failed to format wallet X-chain address: %w", err)
}
// Build P-chain address for wallet
walletPAddr, err := address.Format("P", hrp, walletKey.PChainAddr[:])
if err != nil {
return network.Config{}, fmt.Errorf("failed to format wallet P-chain address: %w", err)
}
// X-chain allocation for wallet (2B LUX)
allocations = append(allocations, map[string]interface{}{
"ethAddr": walletKey.CChainAddrHex(),
"luxAddr": walletXAddr,
"initialAmount": uint64(2000000000000000000), // 2B LUX
"unlockSchedule": []map[string]interface{}{},
})
// P-chain allocation for wallet (2B LUX) - MUST use unlockSchedule
allocations = append(allocations, map[string]interface{}{
"ethAddr": walletKey.CChainAddrHex(),
"luxAddr": walletPAddr,
"initialAmount": uint64(0),
"unlockSchedule": []map[string]interface{}{
{
"amount": uint64(2000000000000000000), // 2B LUX
"locktime": uint64(0), // locktime 0 = immediately unlocked
},
},
})
fmt.Printf(" Wallet: %s -> X:%s P:%s (2B each)\n", walletKey.PChainAddr.String(), walletXAddr, walletPAddr)
}
// Also add allocations for LUX_PRIVATE_KEY if set and different from wallet key
// This is needed when getDefaultKey() in blockchain.go uses LUX_PRIVATE_KEY
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()
privKeyAddr := ids.ShortID(pubKey.Address())
// Check if this is different from wallet key
if privKeyAddr != walletKey.PChainAddr {
privKeyXAddr, errX := address.Format("X", hrp, privKeyAddr[:])
privKeyPAddr, errP := address.Format("P", hrp, privKeyAddr[:])
if errX == nil && errP == nil {
fmt.Printf("🔑 Adding LUX_PRIVATE_KEY allocations (with %s HRP): X=%s P=%s (2B each)\n", hrp, privKeyXAddr, privKeyPAddr)
ethAddr := "0x" + hex.EncodeToString(privKeyAddr[:])
// X-chain allocation
allocations = append(allocations, map[string]interface{}{
"ethAddr": ethAddr,
"luxAddr": privKeyXAddr,
"initialAmount": uint64(2000000000000000000), // 2B LUX
"unlockSchedule": []map[string]interface{}{},
})
// P-chain allocation - MUST use unlockSchedule
allocations = append(allocations, map[string]interface{}{
"ethAddr": ethAddr,
"luxAddr": privKeyPAddr,
"initialAmount": uint64(0),
"unlockSchedule": []map[string]interface{}{
{
"amount": uint64(2000000000000000000), // 2B LUX
"locktime": uint64(0), // locktime 0 = immediately unlocked
},
},
})
}
} else {
fmt.Printf("🔑 LUX_PRIVATE_KEY matches wallet key, no extra allocation needed\n")
}
}
}
}
genesis["initialStakers"] = initialStakers
genesis["allocations"] = allocations
// CRITICAL: Also update xChainGenesis.allocations!
// The X-chain reads allocations from the embedded xChainGenesis JSON string,
// NOT from the top-level allocations array. We must update both.
if xChainGenesisStr, ok := genesis["xChainGenesis"].(string); ok {
var xChainGenesis map[string]interface{}
if err := json.Unmarshal([]byte(xChainGenesisStr), &xChainGenesis); err == nil {
// Build X-chain specific allocations (only X-chain addresses, use avaxAddr format)
xAllocations := make([]map[string]interface{}, 0)
for _, alloc := range allocations {
allocMap := alloc.(map[string]interface{})
luxAddr, _ := allocMap["luxAddr"].(string)
// Only include X-chain allocations (start with "X-")
if strings.HasPrefix(luxAddr, "X-") {
initialAmount, _ := allocMap["initialAmount"].(uint64)
if initialAmount > 0 {
xAllocations = append(xAllocations, map[string]interface{}{
"avaxAddr": luxAddr, // X-chain uses avaxAddr field name
"ethAddr": allocMap["ethAddr"],
"initialAmount": initialAmount,
"unlockSchedule": []interface{}{},
})
}
}
}
xChainGenesis["allocations"] = xAllocations
if updatedXChain, err := json.Marshal(xChainGenesis); err == nil {
genesis["xChainGenesis"] = string(updatedXChain)
fmt.Printf("✅ Updated xChainGenesis with %d allocations\n", len(xAllocations))
}
}
}
// 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
// Start with default flags from netConfig.Flags and add per-node overrides
netConfig.NodeConfigs = make([]node.Config, numNodes)
for i, vk := range validatorKeys {
port := 9630 + int(i)*2
// Copy default network flags to each node
nodeFlags := make(map[string]interface{})
for k, v := range netConfig.Flags {
nodeFlags[k] = v
}
// Add per-node specific flags
nodeFlags[config.HTTPPortKey] = port
nodeFlags[config.StakingPortKey] = port + 1
// Enable sybil protection for mainnet - validators have proper keys
// The genesis validators have matching NodeIDs from deterministic TLS certs
nodeFlags[config.SybilProtectionEnabledKey] = true
netConfig.NodeConfigs[i] = node.Config{
Flags: nodeFlags,
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{},
RedirectStdout: true,
RedirectStderr: true,
}
}
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, constants.MainnetID, numNodes)
}
// NewTestnetConfigFromMnemonic creates testnet config from LUX_MNEMONIC
func NewTestnetConfigFromMnemonic(binaryPath string, numNodes uint32) (network.Config, error) {
return NewConfigFromMnemonic(binaryPath, constants.TestnetID, 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.CustomID, numNodes)
}