Files

217 lines
6.6 KiB
Go

// Copyright (C) 2022-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package keycmd
import (
"encoding/json"
"fmt"
"os"
"strings"
"github.com/luxfi/cli/pkg/key"
"github.com/luxfi/cli/pkg/ux"
"github.com/luxfi/constants"
"github.com/spf13/cobra"
)
var (
deriveCount int
derivePrefix string
deriveStart int
deriveShow bool
deriveExport bool
deriveNetwork string
)
func newDeriveCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "derive",
Short: "Derive keys from MNEMONIC environment variable",
Long: `Derive multiple key sets from a single mnemonic phrase.
Uses the MNEMONIC environment variable to derive keys deterministically.
Each key uses Lux P/X-Chain BIP-44 path: m/44'/9000'/0'/0/{index}
This ensures compatibility with MetaMask, cast, and other Ethereum tools.
The same mnemonic always produces the same keys across all tools.
Examples:
# Derive 5 validator keys from mnemonic
export MNEMONIC="your 24 words here"
lux key derive -n 5 --prefix validator
# Derive keys starting at index 5
lux key derive -n 3 --start 5 --prefix backup
# Show addresses without saving (for verification)
lux key derive -n 5 --show
# Show addresses with private keys (DANGER)
lux key derive -n 1 --show --export`,
RunE: runDerive,
}
cmd.Flags().IntVarP(&deriveCount, "count", "n", 5, "Number of keys to derive")
cmd.Flags().StringVarP(&derivePrefix, "prefix", "p", "mainnet-key", "Prefix for key names")
cmd.Flags().IntVarP(&deriveStart, "start", "s", 0, "Starting account index")
cmd.Flags().BoolVar(&deriveShow, "show", false, "Only show addresses, don't save keys")
cmd.Flags().BoolVar(&deriveExport, "export", false, "Show private keys in output (DANGER - keep secret!)")
cmd.Flags().StringVar(&deriveNetwork, "network", "mainnet",
"Network for P/X address HRP: mainnet (P-lux1…) | testnet (P-test1…) | "+
"devnet (P-dev1…) | local (P-local1…) | custom (P-custom1…)")
return cmd
}
// ValidatorKeyInfo represents exported validator key information.
//
// EVMAddress is the canonical 20-byte account address used by every
// EVM-runtime chain (Lux C-Chain, Hanzo EVM, downstream EVM L1s, and so on).
// The derivation hashes the secp256k1 pubkey with Keccak256 — that's
// HOW. The value IS "EVM-runtime account address" — that's WHAT.
type ValidatorKeyInfo struct {
Index uint32 `json:"index"`
PrivateKey string `json:"private_key,omitempty"`
EVMAddress string `json:"evm_address"`
PChain string `json:"p_chain"`
XChain string `json:"x_chain"`
ShortID string `json:"short_id"`
}
func runDerive(_ *cobra.Command, _ []string) error {
// Get mnemonic from environment
mnemonic := key.GetMnemonicFromEnv()
if mnemonic == "" {
return fmt.Errorf("MNEMONIC environment variable not set or invalid")
}
// Mask the mnemonic in output (show first and last word)
words := strings.Fields(mnemonic)
maskedMnemonic := fmt.Sprintf("%s ... %s (%d words)", words[0], words[len(words)-1], len(words))
ux.Logger.PrintToUser("")
ux.Logger.PrintToUser("Deriving %d keys from mnemonic: %s", deriveCount, maskedMnemonic)
ux.Logger.PrintToUser("BIP-44 path: m/44'/9000'/0'/0/{index} (Lux P/X-Chain)")
ux.Logger.PrintToUser("")
// Resolve network ID for HRP-based address formatting. Pass the
// PRIMARY-NETWORK ID (1/2/3/1337), NOT the EVM chainID (96369).
// Lux HRP is keyed off the P-Chain network ID (coin type 9000
// canonical convention); EVM chain ID is a different namespace.
var networkID uint32
switch deriveNetwork {
case "mainnet":
networkID = constants.MainnetID
case "testnet":
networkID = constants.TestnetID
case "devnet":
networkID = constants.DevnetID
case "local":
networkID = constants.LocalID
case "custom":
networkID = constants.CustomID
default:
return fmt.Errorf("unknown --network %q (want mainnet|testnet|devnet|local|custom)", deriveNetwork)
}
var results []ValidatorKeyInfo
for i := 0; i < deriveCount; i++ {
accountIndex := uint32(deriveStart + i) //nolint:gosec // G115: Index values are bounded by BIP-44 limits
name := fmt.Sprintf("%s-%02d", derivePrefix, accountIndex+1)
// Derive key using BIP-44 path with account index
sf, err := key.NewSoftFromMnemonicWithAccount(networkID, mnemonic, accountIndex)
if err != nil {
return fmt.Errorf("failed to derive key for index %d: %w", accountIndex, err)
}
// Get addresses
pAddrs := sf.P()
xAddrs := sf.X()
cAddr := sf.C()
shortAddrs := sf.Addresses()
pAddr := ""
if len(pAddrs) > 0 {
pAddr = pAddrs[0]
}
xAddr := ""
if len(xAddrs) > 0 {
xAddr = xAddrs[0]
}
shortID := ""
if len(shortAddrs) > 0 {
shortID = fmt.Sprintf("%x", shortAddrs[0][:])
}
info := ValidatorKeyInfo{
Index: accountIndex,
EVMAddress: cAddr,
PChain: pAddr,
XChain: xAddr,
ShortID: shortID,
}
if deriveExport {
info.PrivateKey = sf.PrivKeyHex()
}
results = append(results, info)
if !deriveShow {
// Create HDKeySet for saving
keySet := &key.HDKeySet{
Name: name,
Mnemonic: mnemonic,
ECPrivateKey: sf.Raw(),
ECPublicKey: sf.Key().PublicKey().Bytes(),
ECAddress: cAddr,
}
// Save through encrypted backend
if err := key.SaveKeySet(keySet); err != nil {
ux.Logger.PrintToUser("Warning: failed to save key %s: %v", name, err)
}
}
ux.Logger.PrintToUser("Account %d:", accountIndex)
ux.Logger.PrintToUser(" Name: %s", name)
ux.Logger.PrintToUser(" C-Chain: %s", cAddr)
ux.Logger.PrintToUser(" P-Chain: %s", pAddr)
if deriveExport {
ux.Logger.PrintToUser(" Private: 0x%s", sf.PrivKeyHex())
}
if !deriveShow {
ux.Logger.PrintToUser(" Saved: ✓")
}
ux.Logger.PrintToUser("")
}
// Export validator info for genesis use
if !deriveShow {
validatorsPath := os.ExpandEnv("$HOME/.lux/keys/mainnet_validators.json")
data, err := json.MarshalIndent(results, "", " ")
if err == nil {
if err := os.WriteFile(validatorsPath, data, 0o600); err != nil {
ux.Logger.PrintToUser("Warning: failed to write validators file: %v", err)
} else {
ux.Logger.PrintToUser("Validator info exported to: %s", validatorsPath)
}
}
}
ux.Logger.PrintToUser("")
ux.Logger.PrintToUser("First 5 accounts can be used as genesis validators:")
ux.Logger.PrintToUser(" Account 0: Primary deployer key (pays for transactions)")
ux.Logger.PrintToUser(" Accounts 0-4: Bootstrap validators (need P-Chain allocations)")
ux.Logger.PrintToUser("")
if deriveShow {
ux.Logger.PrintToUser("Run without --show to save keys to encrypted storage")
} else {
ux.Logger.PrintToUser("Keys saved. Use 'lux key list' to see all keys")
}
return nil
}