mirror of
https://github.com/luxfi/keys.git
synced 2026-07-26 23:58:11 +00:00
624 lines
19 KiB
Go
624 lines
19 KiB
Go
// Copyright (C) 2024-2025, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
// Package keys provides validator key management for Lux networks.
|
|
// It handles generation, loading, and storage of:
|
|
// - TLS staking keys (for node identity)
|
|
// - BLS signer keys (for validator consensus)
|
|
// - EC private keys (for P/X/C-chain addresses)
|
|
package keys
|
|
|
|
import (
|
|
"crypto/ecdsa"
|
|
"crypto/elliptic"
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"math/big"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/luxfi/crypto/bls/signer/localsigner"
|
|
luxcrypto "github.com/luxfi/crypto/secp256k1"
|
|
"github.com/luxfi/go-bip32"
|
|
"github.com/luxfi/go-bip39"
|
|
"github.com/luxfi/ids"
|
|
luxtls "github.com/luxfi/tls"
|
|
"github.com/luxfi/vm/platformvm/signer"
|
|
"golang.org/x/crypto/sha3"
|
|
)
|
|
|
|
// ValidatorKey contains all keys needed for a validator node
|
|
type ValidatorKey struct {
|
|
// NodeID is the unique identifier for the node (derived from TLS cert)
|
|
NodeID ids.NodeID
|
|
|
|
// TLS keys for node identity
|
|
StakerKey []byte // PEM-encoded private key
|
|
StakerCert []byte // PEM-encoded certificate
|
|
|
|
// BLS keys for consensus
|
|
BLSSecretKey []byte // Raw BLS secret key bytes
|
|
BLSPublicKey []byte // Compressed BLS public key
|
|
BLSPoP []byte // Proof of Possession signature
|
|
|
|
// EC key for addresses
|
|
ECPrivateKey []byte // Raw 32-byte secp256k1 private key
|
|
|
|
// Derived addresses
|
|
PChainAddr ids.ShortID // P/X chain address (20 bytes)
|
|
CChainAddr ids.ShortID // C-chain address (20 bytes, Ethereum format)
|
|
}
|
|
|
|
// KeyStore manages validator keys with filesystem persistence
|
|
type KeyStore struct {
|
|
baseDir string
|
|
}
|
|
|
|
// NewKeyStore creates a new key store at the given directory
|
|
func NewKeyStore(baseDir string) *KeyStore {
|
|
if baseDir == "" {
|
|
home, _ := os.UserHomeDir()
|
|
baseDir = filepath.Join(home, ".lux", "keys")
|
|
}
|
|
return &KeyStore{baseDir: baseDir}
|
|
}
|
|
|
|
// BaseDir returns the base directory for the key store
|
|
func (ks *KeyStore) BaseDir() string {
|
|
return ks.baseDir
|
|
}
|
|
|
|
// GenerateValidatorKey creates a complete set of validator keys
|
|
func GenerateValidatorKey() (*ValidatorKey, error) {
|
|
vk := &ValidatorKey{}
|
|
|
|
// 1. Generate TLS staking key
|
|
certPEM, keyPEM, err := luxtls.NewCertAndKeyBytes()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to generate TLS cert: %w", err)
|
|
}
|
|
|
|
vk.StakerCert = certPEM
|
|
vk.StakerKey = keyPEM
|
|
|
|
// Parse cert to derive NodeID
|
|
tlsCert, err := luxtls.LoadTLSCertFromBytes(keyPEM, certPEM)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse TLS cert: %w", err)
|
|
}
|
|
stakingCert := &ids.Certificate{
|
|
Raw: tlsCert.Leaf.Raw,
|
|
PublicKey: tlsCert.Leaf.PublicKey,
|
|
}
|
|
vk.NodeID = ids.NodeIDFromCert(stakingCert)
|
|
|
|
// 2. Generate BLS signer key
|
|
blsKey, err := localsigner.New()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to generate BLS key: %w", err)
|
|
}
|
|
vk.BLSSecretKey = blsKey.ToBytes()
|
|
|
|
pop, err := signer.NewProofOfPossession(blsKey)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to generate BLS PoP: %w", err)
|
|
}
|
|
vk.BLSPublicKey = pop.PublicKey[:]
|
|
vk.BLSPoP = pop.ProofOfPossession[:]
|
|
|
|
// 3. Generate EC private key for addresses
|
|
ecKey, err := luxcrypto.NewPrivateKey()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to generate EC key: %w", err)
|
|
}
|
|
vk.ECPrivateKey = ecKey.Bytes()
|
|
|
|
// Derive P-chain address
|
|
pubKey := ecKey.PublicKey()
|
|
vk.PChainAddr = ids.ShortID(pubKey.Address())
|
|
|
|
// Derive C-chain (Ethereum) address
|
|
ecdsaPubKey := pubKey.ToECDSA()
|
|
vk.CChainAddr = pubkeyToAddress(ecdsaPubKey)
|
|
|
|
return vk, nil
|
|
}
|
|
|
|
// pubkeyToAddress derives an Ethereum address from an ECDSA public key
|
|
func pubkeyToAddress(pub *ecdsa.PublicKey) ids.ShortID {
|
|
// Ethereum address is last 20 bytes of Keccak256(uncompressed pubkey without prefix)
|
|
pubBytes := make([]byte, 64)
|
|
copy(pubBytes[:32], pub.X.Bytes())
|
|
copy(pubBytes[32:], pub.Y.Bytes())
|
|
|
|
h := sha3.NewLegacyKeccak256()
|
|
h.Write(pubBytes)
|
|
hash := h.Sum(nil)
|
|
|
|
var addr ids.ShortID
|
|
copy(addr[:], hash[12:32])
|
|
return addr
|
|
}
|
|
|
|
// Save persists a validator key to the filesystem
|
|
func (ks *KeyStore) Save(name string, vk *ValidatorKey) error {
|
|
nodeDir := filepath.Join(ks.baseDir, name)
|
|
|
|
// Create directory structure
|
|
dirs := []string{
|
|
nodeDir,
|
|
filepath.Join(nodeDir, "staking"),
|
|
filepath.Join(nodeDir, "bls"),
|
|
filepath.Join(nodeDir, "ec"),
|
|
}
|
|
for _, dir := range dirs {
|
|
if err := os.MkdirAll(dir, 0700); err != nil {
|
|
return fmt.Errorf("failed to create directory %s: %w", dir, err)
|
|
}
|
|
}
|
|
|
|
// Save TLS staking key and cert
|
|
if err := os.WriteFile(filepath.Join(nodeDir, "staking", "staker.key"), vk.StakerKey, 0600); err != nil {
|
|
return fmt.Errorf("failed to write staker.key: %w", err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(nodeDir, "staking", "staker.crt"), vk.StakerCert, 0644); err != nil {
|
|
return fmt.Errorf("failed to write staker.crt: %w", err)
|
|
}
|
|
|
|
// Also save to legacy paths for backward compatibility
|
|
if err := os.WriteFile(filepath.Join(nodeDir, "staker.key"), vk.StakerKey, 0600); err != nil {
|
|
return fmt.Errorf("failed to write staker.key (legacy): %w", err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(nodeDir, "staker.crt"), vk.StakerCert, 0644); err != nil {
|
|
return fmt.Errorf("failed to write staker.crt (legacy): %w", err)
|
|
}
|
|
|
|
// Save BLS signer key
|
|
if err := os.WriteFile(filepath.Join(nodeDir, "bls", "signer.key"), vk.BLSSecretKey, 0600); err != nil {
|
|
return fmt.Errorf("failed to write signer.key: %w", err)
|
|
}
|
|
|
|
// Save EC private key (hex encoded)
|
|
ecKeyHex := hex.EncodeToString(vk.ECPrivateKey)
|
|
if err := os.WriteFile(filepath.Join(nodeDir, "ec", "private.key"), []byte(ecKeyHex), 0600); err != nil {
|
|
return fmt.Errorf("failed to write private.key: %w", err)
|
|
}
|
|
|
|
// Save key info JSON for reference
|
|
info := fmt.Sprintf(`{
|
|
"nodeID": "%s",
|
|
"pChainAddr": "%s",
|
|
"cChainAddr": "0x%s",
|
|
"blsPublicKey": "0x%s"
|
|
}
|
|
`, vk.NodeID.String(),
|
|
vk.PChainAddr.String(),
|
|
hex.EncodeToString(vk.CChainAddr[:]),
|
|
hex.EncodeToString(vk.BLSPublicKey))
|
|
if err := os.WriteFile(filepath.Join(nodeDir, "info.json"), []byte(info), 0644); err != nil {
|
|
return fmt.Errorf("failed to write info.json: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Load reads a validator key from the filesystem
|
|
func (ks *KeyStore) Load(name string) (*ValidatorKey, error) {
|
|
nodeDir := filepath.Join(ks.baseDir, name)
|
|
return LoadFromDir(nodeDir)
|
|
}
|
|
|
|
// LoadFromDir loads a validator key from a specific directory
|
|
func LoadFromDir(nodeDir string) (*ValidatorKey, error) {
|
|
vk := &ValidatorKey{}
|
|
|
|
// Load TLS cert - try modern path first, then legacy
|
|
certPath := filepath.Join(nodeDir, "staking", "staker.crt")
|
|
certPEM, err := os.ReadFile(certPath)
|
|
if err != nil {
|
|
certPath = filepath.Join(nodeDir, "staker.crt")
|
|
certPEM, err = os.ReadFile(certPath)
|
|
}
|
|
|
|
// Load TLS key
|
|
keyPath := filepath.Join(nodeDir, "staking", "staker.key")
|
|
keyPEM, kerr := os.ReadFile(keyPath)
|
|
if kerr != nil {
|
|
keyPath = filepath.Join(nodeDir, "staker.key")
|
|
keyPEM, kerr = os.ReadFile(keyPath)
|
|
}
|
|
|
|
// If TLS cert/key missing, generate them and persist
|
|
if err != nil || kerr != nil || len(certPEM) == 0 || len(keyPEM) == 0 {
|
|
fmt.Printf(" Generating TLS staking cert for %s\n", filepath.Base(nodeDir))
|
|
certPEM, keyPEM, err = luxtls.NewCertAndKeyBytes()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to generate TLS cert: %w", err)
|
|
}
|
|
// Save to disk for future use
|
|
stakingDir := filepath.Join(nodeDir, "staking")
|
|
if err := os.MkdirAll(stakingDir, 0700); err != nil {
|
|
return nil, fmt.Errorf("failed to create staking dir: %w", err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(stakingDir, "staker.key"), keyPEM, 0600); err != nil {
|
|
return nil, fmt.Errorf("failed to write staker.key: %w", err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(stakingDir, "staker.crt"), certPEM, 0644); err != nil {
|
|
return nil, fmt.Errorf("failed to write staker.crt: %w", err)
|
|
}
|
|
}
|
|
vk.StakerCert = certPEM
|
|
vk.StakerKey = keyPEM
|
|
|
|
// Derive NodeID from TLS cert
|
|
tlsCert, err := luxtls.LoadTLSCertFromBytes(keyPEM, certPEM)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to load TLS cert: %w", err)
|
|
}
|
|
stakingCert := &ids.Certificate{
|
|
Raw: tlsCert.Leaf.Raw,
|
|
PublicKey: tlsCert.Leaf.PublicKey,
|
|
}
|
|
vk.NodeID = ids.NodeIDFromCert(stakingCert)
|
|
|
|
// Load BLS signer key (optional)
|
|
signerPath := filepath.Join(nodeDir, "bls", "signer.key")
|
|
signerBytes, err := os.ReadFile(signerPath)
|
|
if err != nil {
|
|
signerPath = filepath.Join(nodeDir, "signer.key")
|
|
signerBytes, _ = os.ReadFile(signerPath)
|
|
}
|
|
if len(signerBytes) > 0 {
|
|
vk.BLSSecretKey = signerBytes
|
|
// Derive public key and PoP using localsigner + signer.NewProofOfPossession
|
|
// This must match how keys are generated in GenerateValidatorKey/DeriveValidatorFromMnemonic
|
|
blsSigner, err := localsigner.FromBytes(signerBytes)
|
|
if err == nil {
|
|
pop, err := signer.NewProofOfPossession(blsSigner)
|
|
if err == nil {
|
|
vk.BLSPublicKey = pop.PublicKey[:]
|
|
vk.BLSPoP = pop.ProofOfPossession[:]
|
|
}
|
|
}
|
|
}
|
|
|
|
// Load EC private key (optional)
|
|
ecPath := filepath.Join(nodeDir, "ec", "private.key")
|
|
ecKeyHex, err := os.ReadFile(ecPath)
|
|
if err != nil {
|
|
ecPath = filepath.Join(nodeDir, "private.key")
|
|
ecKeyHex, _ = os.ReadFile(ecPath)
|
|
}
|
|
if len(ecKeyHex) > 0 {
|
|
privKeyBytes, err := hex.DecodeString(strings.TrimSpace(string(ecKeyHex)))
|
|
if err == nil && len(privKeyBytes) == 32 {
|
|
vk.ECPrivateKey = privKeyBytes
|
|
|
|
// Derive addresses
|
|
luxPrivKey, err := luxcrypto.ToPrivateKey(privKeyBytes)
|
|
if err == nil {
|
|
pubKey := luxPrivKey.PublicKey()
|
|
vk.PChainAddr = ids.ShortID(pubKey.Address())
|
|
vk.CChainAddr = pubkeyToAddress(pubKey.ToECDSA())
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fallback: derive addresses from NodeID if EC key not available
|
|
if vk.PChainAddr == (ids.ShortID{}) {
|
|
copy(vk.PChainAddr[:], vk.NodeID[:20])
|
|
copy(vk.CChainAddr[:], vk.NodeID[:20])
|
|
}
|
|
|
|
return vk, nil
|
|
}
|
|
|
|
// List returns all validator keys in the store
|
|
func (ks *KeyStore) List() ([]string, error) {
|
|
entries, err := os.ReadDir(ks.baseDir)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return nil, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
var names []string
|
|
for _, entry := range entries {
|
|
if entry.IsDir() {
|
|
name := entry.Name()
|
|
// Skip hidden directories (like .git) and non-node directories
|
|
if strings.HasPrefix(name, ".") {
|
|
continue
|
|
}
|
|
// Only include node* directories
|
|
if !strings.HasPrefix(name, "node") {
|
|
continue
|
|
}
|
|
names = append(names, name)
|
|
}
|
|
}
|
|
return names, nil
|
|
}
|
|
|
|
// GenerateMultiple generates multiple validator keys
|
|
func (ks *KeyStore) GenerateMultiple(count int, prefix string) ([]*ValidatorKey, error) {
|
|
keys := make([]*ValidatorKey, count)
|
|
for i := 0; i < count; i++ {
|
|
vk, err := GenerateValidatorKey()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to generate key %d: %w", i, err)
|
|
}
|
|
keys[i] = vk
|
|
|
|
name := fmt.Sprintf("%s%d", prefix, i+1)
|
|
if err := ks.Save(name, vk); err != nil {
|
|
return nil, fmt.Errorf("failed to save key %s: %w", name, err)
|
|
}
|
|
}
|
|
return keys, nil
|
|
}
|
|
|
|
// LoadAll loads all validator keys from the store
|
|
func (ks *KeyStore) LoadAll() ([]*ValidatorKey, error) {
|
|
names, err := ks.List()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
keys := make([]*ValidatorKey, 0, len(names))
|
|
for _, name := range names {
|
|
vk, err := ks.Load(name)
|
|
if err != nil {
|
|
continue // Skip invalid entries
|
|
}
|
|
keys = append(keys, vk)
|
|
}
|
|
return keys, nil
|
|
}
|
|
|
|
// BLSKeyBase64 returns the BLS secret key as base64 (for node config)
|
|
func (vk *ValidatorKey) BLSKeyBase64() string {
|
|
return base64.StdEncoding.EncodeToString(vk.BLSSecretKey)
|
|
}
|
|
|
|
// BLSPublicKeyHex returns the BLS public key as hex with 0x prefix
|
|
func (vk *ValidatorKey) BLSPublicKeyHex() string {
|
|
return "0x" + hex.EncodeToString(vk.BLSPublicKey)
|
|
}
|
|
|
|
// BLSPoPHex returns the BLS proof of possession as hex with 0x prefix
|
|
func (vk *ValidatorKey) BLSPoPHex() string {
|
|
return "0x" + hex.EncodeToString(vk.BLSPoP)
|
|
}
|
|
|
|
// CChainAddrHex returns the C-chain address as hex with 0x prefix
|
|
func (vk *ValidatorKey) CChainAddrHex() string {
|
|
return "0x" + hex.EncodeToString(vk.CChainAddr[:])
|
|
}
|
|
|
|
// DeriveValidatorsFromMnemonic derives N validator keys from a BIP39 mnemonic.
|
|
// Each validator uses BIP44 path m/44'/9000'/0'/0/{index} for the EC key.
|
|
// TLS staking certs and BLS keys are generated fresh (not deterministic from mnemonic).
|
|
// This is designed for runtime use - no files are written to disk.
|
|
func DeriveValidatorsFromMnemonic(mnemonic string, count int) ([]*ValidatorKey, error) {
|
|
if count <= 0 || count > 100 {
|
|
return nil, fmt.Errorf("invalid validator count: %d (must be 1-100)", count)
|
|
}
|
|
|
|
validators := make([]*ValidatorKey, count)
|
|
|
|
for i := 0; i < count; i++ {
|
|
vk, err := DeriveValidatorFromMnemonic(mnemonic, uint32(i))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to derive validator %d: %w", i, err)
|
|
}
|
|
validators[i] = vk
|
|
}
|
|
|
|
return validators, nil
|
|
}
|
|
|
|
// DeriveValidatorFromMnemonic derives a single validator key from mnemonic at given index.
|
|
// All keys (EC, TLS, BLS) are now derived deterministically from the mnemonic.
|
|
func DeriveValidatorFromMnemonic(mnemonic string, accountIndex uint32) (*ValidatorKey, error) {
|
|
vk := &ValidatorKey{}
|
|
|
|
// 1. Derive EC key from mnemonic using BIP44 path m/44'/9000'/0'/0/{index}
|
|
ecKeyBytes, err := deriveMnemonicKey(mnemonic, accountIndex)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to derive EC key: %w", err)
|
|
}
|
|
vk.ECPrivateKey = ecKeyBytes
|
|
|
|
// Derive P-chain and C-chain addresses
|
|
luxPrivKey, err := luxcrypto.ToPrivateKey(ecKeyBytes)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create secp256k1 key: %w", err)
|
|
}
|
|
pubKey := luxPrivKey.PublicKey()
|
|
vk.PChainAddr = ids.ShortID(pubKey.Address())
|
|
vk.CChainAddr = pubkeyToAddress(pubKey.ToECDSA())
|
|
|
|
// 2. Derive TLS staking cert deterministically from mnemonic
|
|
// Use a separate derivation path: m/44'/9000'/1'/0/{index} for TLS keys
|
|
tlsKeyBytes, err := deriveMnemonicKeyForPath(mnemonic, 1, accountIndex) // account=1 for TLS
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to derive TLS key seed: %w", err)
|
|
}
|
|
|
|
// Create P-256 private key from derived seed (TLS uses P-256, not secp256k1)
|
|
p256Key, err := deriveP256Key(tlsKeyBytes)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to derive P-256 key: %w", err)
|
|
}
|
|
|
|
certPEM, keyPEM, err := luxtls.NewCertAndKeyBytesFromKey(p256Key)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to generate TLS cert: %w", err)
|
|
}
|
|
vk.StakerCert = certPEM
|
|
vk.StakerKey = keyPEM
|
|
|
|
// Derive NodeID from TLS cert
|
|
tlsCert, err := luxtls.LoadTLSCertFromBytes(keyPEM, certPEM)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse TLS cert: %w", err)
|
|
}
|
|
stakingCert := &ids.Certificate{
|
|
Raw: tlsCert.Leaf.Raw,
|
|
PublicKey: tlsCert.Leaf.PublicKey,
|
|
}
|
|
vk.NodeID = ids.NodeIDFromCert(stakingCert)
|
|
|
|
// 3. Derive BLS signer key deterministically from mnemonic
|
|
// Use a separate derivation path: m/44'/9000'/2'/0/{index} for BLS keys
|
|
blsSeed, err := deriveMnemonicKeyForPath(mnemonic, 2, accountIndex) // account=2 for BLS
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to derive BLS key seed: %w", err)
|
|
}
|
|
|
|
// Create BLS signer from seed using proper BLS key derivation (handles field order internally)
|
|
blsKey, err := localsigner.FromSeed(blsSeed)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create BLS key from seed: %w", err)
|
|
}
|
|
vk.BLSSecretKey = blsKey.ToBytes()
|
|
|
|
pop, err := signer.NewProofOfPossession(blsKey)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to generate BLS PoP: %w", err)
|
|
}
|
|
vk.BLSPublicKey = pop.PublicKey[:]
|
|
vk.BLSPoP = pop.ProofOfPossession[:]
|
|
|
|
return vk, nil
|
|
}
|
|
|
|
// deriveP256Key creates an ECDSA P-256 private key from a 32-byte seed.
|
|
// This allows deterministic TLS key generation from mnemonic-derived seeds.
|
|
func deriveP256Key(seed []byte) (*ecdsa.PrivateKey, error) {
|
|
if len(seed) < 32 {
|
|
return nil, fmt.Errorf("seed must be at least 32 bytes")
|
|
}
|
|
|
|
// Use the seed as the private key scalar (reduced mod curve order)
|
|
curve := elliptic.P256()
|
|
k := new(big.Int).SetBytes(seed[:32])
|
|
k.Mod(k, curve.Params().N)
|
|
|
|
// Ensure k is not zero
|
|
if k.Sign() == 0 {
|
|
k.SetInt64(1)
|
|
}
|
|
|
|
priv := &ecdsa.PrivateKey{
|
|
PublicKey: ecdsa.PublicKey{
|
|
Curve: curve,
|
|
},
|
|
D: k,
|
|
}
|
|
priv.PublicKey.X, priv.PublicKey.Y = curve.ScalarBaseMult(k.Bytes())
|
|
|
|
return priv, nil
|
|
}
|
|
|
|
// deriveMnemonicKeyForPath derives a key using BIP44 path m/44'/9000'/{account}'/0/{index}
|
|
func deriveMnemonicKeyForPath(mnemonic string, account, index uint32) ([]byte, error) {
|
|
if !bip39.IsMnemonicValid(mnemonic) {
|
|
return nil, fmt.Errorf("invalid mnemonic phrase")
|
|
}
|
|
seed := bip39.NewSeed(mnemonic, "")
|
|
|
|
// Create master key from seed
|
|
masterKey, err := bip32.NewMasterKey(seed)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create master key: %w", err)
|
|
}
|
|
|
|
// BIP-44 path: m/44'/9000'/{account}'/0/{index}
|
|
// m/44' (purpose)
|
|
key, err := masterKey.NewChildKey(bip32.FirstHardenedChild + 44)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to derive purpose: %w", err)
|
|
}
|
|
|
|
// m/44'/9000' (coin type for LUX)
|
|
key, err = key.NewChildKey(bip32.FirstHardenedChild + LUXCoinType)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to derive coin type: %w", err)
|
|
}
|
|
|
|
// m/44'/9000'/{account}' (account - 0=EC, 1=TLS, 2=BLS)
|
|
key, err = key.NewChildKey(bip32.FirstHardenedChild + account)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to derive account: %w", err)
|
|
}
|
|
|
|
// m/44'/9000'/{account}'/0 (change)
|
|
key, err = key.NewChildKey(0)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to derive change: %w", err)
|
|
}
|
|
|
|
// m/44'/9000'/{account}'/0/{index} (address index)
|
|
key, err = key.NewChildKey(index)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to derive address index: %w", err)
|
|
}
|
|
|
|
return key.Key, nil
|
|
}
|
|
|
|
// LUXCoinType is the BIP-44 coin type for LUX (9000')
|
|
const LUXCoinType = 9000
|
|
|
|
// deriveMnemonicKey derives an EC private key from mnemonic using BIP44 path m/44'/9000'/0'/0/{index}
|
|
func deriveMnemonicKey(mnemonic string, accountIndex uint32) ([]byte, error) {
|
|
if !bip39.IsMnemonicValid(mnemonic) {
|
|
return nil, fmt.Errorf("invalid mnemonic phrase")
|
|
}
|
|
seed := bip39.NewSeed(mnemonic, "")
|
|
|
|
// Create master key from seed
|
|
masterKey, err := bip32.NewMasterKey(seed)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create master key: %w", err)
|
|
}
|
|
|
|
// BIP-44 path: m/44'/9000'/0'/0/{accountIndex}
|
|
// m/44' (purpose)
|
|
key, err := masterKey.NewChildKey(bip32.FirstHardenedChild + 44)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to derive purpose: %w", err)
|
|
}
|
|
|
|
// m/44'/9000' (coin type for LUX)
|
|
key, err = key.NewChildKey(bip32.FirstHardenedChild + LUXCoinType)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to derive coin type: %w", err)
|
|
}
|
|
|
|
// m/44'/9000'/0' (account)
|
|
key, err = key.NewChildKey(bip32.FirstHardenedChild + 0)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to derive account: %w", err)
|
|
}
|
|
|
|
// m/44'/9000'/0'/0 (change)
|
|
key, err = key.NewChildKey(0)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to derive change: %w", err)
|
|
}
|
|
|
|
// m/44'/9000'/0'/0/{accountIndex} (address index)
|
|
key, err = key.NewChildKey(accountIndex)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to derive address index: %w", err)
|
|
}
|
|
|
|
return key.Key, nil
|
|
}
|