mirror of
https://github.com/luxfi/netrunner.git
synced 2026-07-27 00:04:23 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user