feat: Load wallet keys from ~/.lux/keys at runtime

- Remove hardcoded genesis.EWOQKey dependency
- Add getDefaultKey() function to load keys from ~/.lux/keys
- Add keys.go module for key management (LoadOrGenerateKeys, GenerateVestingSchedule)
- Update genesis dependency to v1.2.7 (no embedded keys)
- Keys are never embedded in source code

Breaking: Applications must have keys in ~/.lux/keys or they will be auto-generated
This commit is contained in:
Zach Kelling
2025-12-09 21:03:44 +00:00
parent e9ad6d53b7
commit afefff5fcc
6 changed files with 242 additions and 60 deletions
+2 -2
View File
@@ -10,13 +10,13 @@ require (
github.com/luxfi/consensus v1.22.2
github.com/luxfi/crypto v1.17.7
github.com/luxfi/evm v0.8.1
github.com/luxfi/genesis v1.2.5
github.com/luxfi/genesis v1.2.7
github.com/luxfi/geth v1.16.40
github.com/luxfi/ids v1.1.2
github.com/luxfi/log v1.1.24
github.com/luxfi/math v0.1.5
github.com/luxfi/metric v1.4.5
github.com/luxfi/node v1.21.5
github.com/luxfi/node v1.21.6
github.com/onsi/ginkgo/v2 v2.25.1
github.com/onsi/gomega v1.38.0
github.com/otiai10/copy v1.14.1
+4 -4
View File
@@ -285,8 +285,8 @@ github.com/luxfi/database v1.2.7 h1:FpPfvl6C4/DqeP9OrV5LbeUAFxmDyBgHP8CcvSO2D1c=
github.com/luxfi/database v1.2.7/go.mod h1:yZGYMY97Ca0pboIyOQ5JTD/ErvpW4bGot7rUvfMn5ko=
github.com/luxfi/evm v0.8.1 h1:DmQC5UETgunf4e0HGHkeDJCFwU2dOflcHLDICvszN7g=
github.com/luxfi/evm v0.8.1/go.mod h1:ufY7D5sJ9mPUsj1ijuTXUU/ymiMoPoEI1mkjde3H1aA=
github.com/luxfi/genesis v1.2.5 h1:FQLpYZ4Mi3vYZOgLPeJZZMkexIdp+feTrTtSGRUv7Oc=
github.com/luxfi/genesis v1.2.5/go.mod h1:wMBXXh4V3JntkWfQhOI/Rrv4mzGIuC+VN1aDgb6I2iM=
github.com/luxfi/genesis v1.2.7 h1:RT5FpnPOSEzS9cm62mqXlJKjYKmkJWvqIUcjsFhmJ2o=
github.com/luxfi/genesis v1.2.7/go.mod h1:wMBXXh4V3JntkWfQhOI/Rrv4mzGIuC+VN1aDgb6I2iM=
github.com/luxfi/geth v1.16.40 h1:26dLZh0huxAm2/iiMac2pzwh7mlMXyJafW2Tj22qqjc=
github.com/luxfi/geth v1.16.40/go.mod h1:sfmc/YJ0EaaNP5PJmD31yTaSP6VLSvwvOUCViTSZa0I=
github.com/luxfi/ids v1.1.2 h1:+qCUzE9Ga4slSHbnYl7T3I6c8y+n4/kKk4rzSEkLv/k=
@@ -301,8 +301,8 @@ github.com/luxfi/metric v1.4.5 h1:fD6gtpzB5ebGB06/m4K7e5+tOV5RiVHSZCV+Zb8ZDxk=
github.com/luxfi/metric v1.4.5/go.mod h1:AUQ7NSNz9WndAcr/SKnOkP7XSFFnBXOa+ihtJYfDaQY=
github.com/luxfi/mock v0.1.0 h1:IwElfNu+T9sXvzFX6tudPDx1vqPuACRSRdxpD5lxW+o=
github.com/luxfi/mock v0.1.0/go.mod h1:izF+9K0gGzFC9zERn6Po37v46eLdPB+EIsDjL3GLk+U=
github.com/luxfi/node v1.21.5 h1:k9ov99r/kMhWg/ADgM5xv/OxrMK59iw9YtENCi+vTNg=
github.com/luxfi/node v1.21.5/go.mod h1:P8oadRkpiRU0Ezuj48KTUhZRGoJEdB8EbPEeMbXWnR4=
github.com/luxfi/node v1.21.6 h1:JuR+oPjhhaXnY+oS1gA+f/aP0WFReQ2sk2o0tiihpLE=
github.com/luxfi/node v1.21.6/go.mod h1:P8oadRkpiRU0Ezuj48KTUhZRGoJEdB8EbPEeMbXWnR4=
github.com/luxfi/trace v0.1.2 h1:KhRZbk2lQQmmYZjdTWcZKCYkLfu7/VUiLFIsWFKhkwg=
github.com/luxfi/trace v0.1.2/go.mod h1:4SleFc5NVbQYEfn6rYafdfxvHJ+QSdkGAIfKiICYvQE=
github.com/luxfi/warp v1.16.26 h1:XawtVax9UXd9NbIuQt4aY74ypfAvzkypW4LqdIuBnHM=
+28 -3
View File
@@ -6,6 +6,7 @@ package local
import (
"context"
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
"os"
@@ -14,6 +15,7 @@ import (
"strings"
"time"
"github.com/luxfi/genesis/pkg/genesis"
"github.com/luxfi/node/vms/platformvm/reward"
"github.com/luxfi/node/vms/components/lux"
@@ -25,7 +27,7 @@ import (
"github.com/luxfi/netrunner/utils"
"github.com/luxfi/node/api/admin"
"github.com/luxfi/node/config"
"github.com/luxfi/genesis/pkg/genesis"
"github.com/luxfi/crypto/secp256k1"
"github.com/luxfi/ids"
"github.com/luxfi/node/utils/constants"
"github.com/luxfi/math/set"
@@ -671,12 +673,35 @@ type wallet struct {
luxAssetID ids.ID
}
// getDefaultKey loads the first key from ~/.lux/keys for wallet operations.
// Keys are loaded from disk, never hardcoded in source.
func getDefaultKey() (*secp256k1.PrivateKey, error) {
keys, err := LoadOrGenerateKeys("", 1)
if err != nil {
return nil, fmt.Errorf("failed to load keys from ~/.lux/keys: %w", err)
}
if len(keys) == 0 {
return nil, errors.New("no keys found in ~/.lux/keys")
}
// Convert hex to private key
privKeyBytes, err := hex.DecodeString(keys[0].PrivKeyHex)
if err != nil {
return nil, fmt.Errorf("invalid key format: %w", err)
}
return secp256k1.ToPrivateKey(privKeyBytes)
}
func newWallet(
ctx context.Context,
uri string,
preloadTXs []ids.ID,
) (*wallet, error) {
kc := secp256k1fx.NewKeychain(genesis.EWOQKey)
// Load key from ~/.lux/keys - no hardcoded keys
privKey, err := getDefaultKey()
if err != nil {
return nil, fmt.Errorf("failed to get wallet key: %w", err)
}
kc := secp256k1fx.NewKeychain(privKey)
// Use dedicated timeout context for FetchState to avoid parent context cancellation propagation
fetchCtx, fetchCancel := createDefaultCtx(ctx)
luxState, err := primary.FetchState(fetchCtx, uri, kc.Addrs)
@@ -704,7 +729,7 @@ func newWallet(
xChainID := luxState.XCTX.BlockchainID
xUTXOs := common.NewChainUTXOs(xChainID, luxState.UTXOs)
var w wallet
w.addr = genesis.EWOQKey.PublicKey().Address()
w.addr = privKey.PublicKey().Address()
// TODO: Create owners map instead of pTXs
w.pBackend = pwallet.NewBackend(luxState.PCTX, pUTXOs, pTXs)
w.pBuilder = pbuilder.New(kc.Addrs, luxState.PCTX, w.pBackend)
+28 -40
View File
@@ -186,52 +186,40 @@ func NewConfigForNetwork(binaryPath string, numNodes uint32, networkID uint32) (
return network.Config{}, fmt.Errorf("couldn't format stake address: %w", err)
}
// Update allocations to include staked funds if not present
allocations, ok := genesis["allocations"].([]interface{})
if !ok {
allocations = []interface{}{}
}
// Add stake allocation for validators
now := time.Now().Unix()
stakeAllocation := map[string]interface{}{
"ethAddr": "0x0000000000000000000000000000000000000000",
"luxAddr": stakeAddr,
"initialAmount": 0,
"unlockSchedule": []map[string]interface{}{
{
"amount": uint64(numNodes) * 1000000000000, // 1M LUX per validator
"locktime": uint64(now + 7*24*3600), // 1 week lockup
},
},
}
allocations = append(allocations, stakeAllocation)
// Add ewoq allocation with immediately available funds for P-chain operations (subnet creation, etc.)
// Ewoq ShortID: 3cb7d3842e8cee6a0ebd09f1fe884f6861e1b29c
var ewoqShortID ids.ShortID
ewoqBytes, _ := hex.DecodeString("3cb7d3842e8cee6a0ebd09f1fe884f6861e1b29c")
copy(ewoqShortID[:], ewoqBytes)
ewoqAddr, err := address.Format("X", hrp, ewoqShortID[:])
// Load validator keys from ~/.lux/keys/ (generates new ones if missing)
// Each key gets 1B LUX with 1% vesting per year since Jan 1, 2020
validatorKeys, err := LoadOrGenerateKeys("", 5)
if err != nil {
return network.Config{}, fmt.Errorf("couldn't format ewoq address: %w", err)
return network.Config{}, fmt.Errorf("failed to load validator keys: %w", err)
}
// Allocation with unlockSchedule locktime=0 makes funds available on P-chain immediately
ewoqAllocation := map[string]interface{}{
"ethAddr": "0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC", // C-chain address for ewoq
"luxAddr": ewoqAddr,
"initialAmount": uint64(100000000000000000), // 100M LUX on X-chain
"unlockSchedule": []map[string]interface{}{
{
"amount": uint64(100000000000000000), // 100M LUX available immediately on P-chain
"locktime": 0, // Immediately available (no lock)
},
},
// Generate allocations from loaded keys with proper vesting schedule
keyAllocations, err := GenerateAllocationsFromKeys(validatorKeys, hrp)
if err != nil {
return network.Config{}, fmt.Errorf("failed to generate allocations: %w", err)
}
// Convert to []interface{} for JSON marshaling
allocations := make([]interface{}, len(keyAllocations))
for i, alloc := range keyAllocations {
allocations[i] = alloc
}
allocations = append(allocations, ewoqAllocation)
genesis["allocations"] = allocations
// Update C-chain genesis with proper allocations
now := time.Now().Unix()
if cChainGenesisStr, ok := genesis["cChainGenesis"].(string); ok {
var cChainGenesis map[string]interface{}
if err := json.Unmarshal([]byte(cChainGenesisStr), &cChainGenesis); err == nil {
// Update the alloc field with our validator keys
cChainGenesis["alloc"] = GenerateCChainAllocFromKeys(validatorKeys)
// Re-serialize C-chain genesis
if updatedCChain, err := json.Marshal(cChainGenesis); err == nil {
genesis["cChainGenesis"] = string(updatedCChain)
}
}
}
// Set initial staked funds
genesis["initialStakedFunds"] = []string{stakeAddr}
+175
View File
@@ -0,0 +1,175 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package local
import (
"crypto/rand"
"encoding/hex"
"fmt"
"os"
"path/filepath"
"strings"
luxcrypto "github.com/luxfi/crypto/secp256k1"
"github.com/luxfi/geth/crypto"
"github.com/luxfi/ids"
"github.com/luxfi/node/utils/formatting/address"
)
// KeyInfo contains computed addresses from a private key
type KeyInfo struct {
PrivKeyHex string
EthAddr string
ShortID ids.ShortID
}
// DefaultKeyPath returns the default path for lux keys
func DefaultKeyPath() string {
home, _ := os.UserHomeDir()
return filepath.Join(home, ".lux", "keys")
}
// 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
func LoadOrGenerateKeys(keyPath string, count int) ([]KeyInfo, error) {
if keyPath == "" {
keyPath = DefaultKeyPath()
}
// Ensure key directory exists
if err := os.MkdirAll(keyPath, 0700); err != nil {
return nil, fmt.Errorf("failed to create key directory: %w", err)
}
keys := 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))
}
// 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
}
// ComputeKeyInfo derives addresses from a hex-encoded private key
func ComputeKeyInfo(privKeyHex string) (KeyInfo, error) {
privKeyBytes, err := hex.DecodeString(privKeyHex)
if err != nil {
return KeyInfo{}, fmt.Errorf("invalid hex key: %w", err)
}
// Get ETH address
ethPrivKey, err := crypto.ToECDSA(privKeyBytes)
if err != nil {
return KeyInfo{}, fmt.Errorf("invalid ECDSA key: %w", err)
}
ethAddr := crypto.PubkeyToAddress(ethPrivKey.PublicKey)
// Get Lux ShortID (for X/P chain addresses)
luxPrivKey, err := luxcrypto.ToPrivateKey(privKeyBytes)
if err != nil {
return KeyInfo{}, fmt.Errorf("invalid Lux key: %w", err)
}
pubKey := luxPrivKey.PublicKey()
shortID := ids.ShortID(pubKey.Address())
return KeyInfo{
PrivKeyHex: privKeyHex,
EthAddr: ethAddr.Hex(),
ShortID: shortID,
}, nil
}
// FormatAddress formats a ShortID as an X or P chain address with the given HRP
func FormatAddress(chainID string, hrp string, shortID ids.ShortID) (string, error) {
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
}
// Vesting configuration constants
const (
// OneBillionLUX is 1B LUX in nLUX (9 decimals)
OneBillionLUX uint64 = 1_000_000_000_000_000_000
// OnePercentLUX is 1% of 1B = 10M LUX
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
)
// GenerateVestingSchedule creates an unlock schedule with 1% per year for 100 years
// starting from Jan 1, 2020. This means ~5-6% is already unlocked as of Dec 2025.
func GenerateVestingSchedule() []map[string]interface{} {
schedule := make([]map[string]interface{}, 100)
for year := 0; year < 100; year++ {
unlockTime := Jan1_2020 + (uint64(year) * SecondsPerYear)
schedule[year] = map[string]interface{}{
"amount": OnePercentLUX,
"locktime": unlockTime,
}
}
return schedule
}
// GenerateAllocationsFromKeys creates genesis allocations for loaded keys with vesting
func GenerateAllocationsFromKeys(keys []KeyInfo, hrp string) ([]map[string]interface{}, error) {
vestingSchedule := GenerateVestingSchedule()
allocations := make([]map[string]interface{}, len(keys))
for i, key := range keys {
luxAddr, err := FormatAddress("X", hrp, key.ShortID)
if err != nil {
return nil, fmt.Errorf("failed to format address for key %d: %w", i, err)
}
allocations[i] = map[string]interface{}{
"ethAddr": key.EthAddr,
"luxAddr": luxAddr,
"initialAmount": OneBillionLUX, // 1B LUX on X-chain
"unlockSchedule": vestingSchedule,
}
}
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
}
+5 -11
View File
@@ -337,17 +337,11 @@ func newNetwork(
// NewDefaultNetwork returns a new network using a pre-defined
// network configuration.
// The following addresses are pre-funded:
// X-Chain Address 1: X-custom18jma8ppw3nhx5r4ap8clazz0dps7rv5u9xde7p
// X-Chain Address 1 Key: PrivateKey-ewoqjP7PxY4yr3iLTpLisriqt94hdyDFNgchSxGGztUrTXtNN
// X-Chain Address 2: X-custom16045mxr3s2cjycqe2xfluk304xv3ezhkhsvkpr
// X-Chain Address 2 Key: PrivateKey-2fzYBh3bbWemKxQmMfX6DSuL2BFmDSLQWTvma57xwjQjtf8gFq
// P-Chain Address 1: P-custom18jma8ppw3nhx5r4ap8clazz0dps7rv5u9xde7p
// P-Chain Address 1 Key: PrivateKey-ewoqjP7PxY4yr3iLTpLisriqt94hdyDFNgchSxGGztUrTXtNN
// P-Chain Address 2: P-custom16045mxr3s2cjycqe2xfluk304xv3ezhkhsvkpr
// P-Chain Address 2 Key: PrivateKey-2fzYBh3bbWemKxQmMfX6DSuL2BFmDSLQWTvma57xwjQjtf8gFq
// C-Chain Address: 0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC
// C-Chain Address Key: 56289e99c94b6912bfc12adc093c9b51124f0dc54ac7a766b2bc5ccf558d8027
// The following addresses are pre-funded (treasury address):
// X-Chain Address: X-lux1c7wevm4667l4umtzh93r25wpxlpsadkhka6gv6
// P-Chain Address: P-lux1c7wevm4667l4umtzh93r25wpxlpsadkhka6gv6
// C-Chain Address: 0x9011E888251AB053B7bD1cdB598Db4f9DEd94714
// Keys are dynamically generated per network - see ~/.lux/keys/
// The following nodes are validators:
// * NodeID-7Xhw2mDxuDS44j42TCB6U5579esbSt3Lg
// * NodeID-MFrZFVCXPv5iCn6M9K6XduxGTYp891xXZ