mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-04-07 03:28:05 +02:00
btcwallet: add method for deriving key from BIP32 path
This commit is contained in:
parent
ca5d5023e3
commit
167a1f2b79
@ -1,11 +1,14 @@
|
||||
package btcwallet
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/btcsuite/btcutil"
|
||||
"github.com/btcsuite/btcutil/hdkeychain"
|
||||
"github.com/btcsuite/btcwallet/waddrmgr"
|
||||
"github.com/btcsuite/btcwallet/walletdb"
|
||||
"github.com/go-errors/errors"
|
||||
@ -73,6 +76,133 @@ func deriveFromKeyLoc(scopedMgr *waddrmgr.ScopedKeyManager,
|
||||
return addr.(waddrmgr.ManagedPubKeyAddress).PrivKey()
|
||||
}
|
||||
|
||||
// deriveKeyByBIP32Path derives a key described by a BIP32 path. We expect the
|
||||
// first three elements of the path to be hardened according to BIP44, so they
|
||||
// must be a number >= 2^31.
|
||||
func (b *BtcWallet) deriveKeyByBIP32Path(path []uint32) (*btcec.PrivateKey,
|
||||
error) {
|
||||
|
||||
// Make sure we get a full path with exactly 5 elements. A path is
|
||||
// either custom purpose one with 4 dynamic and one static elements:
|
||||
// m/1017'/coinType'/keyFamily'/0/index
|
||||
// Or a default BIP49/89 one with 5 elements:
|
||||
// m/purpose'/coinType'/account'/change/index
|
||||
const expectedDerivationPathDepth = 5
|
||||
if len(path) != expectedDerivationPathDepth {
|
||||
return nil, fmt.Errorf("invalid BIP32 derivation path, "+
|
||||
"expected path length %d, instead was %d",
|
||||
expectedDerivationPathDepth, len(path))
|
||||
}
|
||||
|
||||
// Assert that the first three parts of the path are actually hardened
|
||||
// to avoid under-flowing the uint32 type.
|
||||
if err := assertHardened(path[0], path[1], path[2]); err != nil {
|
||||
return nil, fmt.Errorf("invalid BIP32 derivation path, "+
|
||||
"expected first three elements to be hardened: %w", err)
|
||||
}
|
||||
|
||||
purpose := path[0] - hdkeychain.HardenedKeyStart
|
||||
coinType := path[1] - hdkeychain.HardenedKeyStart
|
||||
account := path[2] - hdkeychain.HardenedKeyStart
|
||||
change, index := path[3], path[4]
|
||||
|
||||
// Is this a custom lnd internal purpose key?
|
||||
switch purpose {
|
||||
case keychain.BIP0043Purpose:
|
||||
// Make sure it's for the same coin type as our wallet's
|
||||
// keychain scope.
|
||||
if coinType != b.chainKeyScope.Coin {
|
||||
return nil, fmt.Errorf("invalid BIP32 derivation "+
|
||||
"path, expected coin type %d, instead was %d",
|
||||
b.chainKeyScope.Coin, coinType)
|
||||
}
|
||||
|
||||
return b.deriveKeyByLocator(keychain.KeyLocator{
|
||||
Family: keychain.KeyFamily(account),
|
||||
Index: index,
|
||||
})
|
||||
|
||||
// Is it a standard, BIP defined purpose that the wallet understands?
|
||||
case waddrmgr.KeyScopeBIP0044.Purpose,
|
||||
waddrmgr.KeyScopeBIP0049Plus.Purpose,
|
||||
waddrmgr.KeyScopeBIP0084.Purpose:
|
||||
|
||||
// We're going to continue below the switch statement to avoid
|
||||
// unnecessary indentation for this default case.
|
||||
|
||||
// Currently, there is no way to import any other key scopes than the
|
||||
// one custom purpose or three standard ones into lnd's wallet. So we
|
||||
// shouldn't accept any other scopes to sign for.
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid BIP32 derivation path, "+
|
||||
"unknown purpose %d", purpose)
|
||||
}
|
||||
|
||||
// Okay, we made sure it's a BIP49/84 key, so we need to derive it now.
|
||||
// Interestingly, the btcwallet never actually uses a coin type other
|
||||
// than 0 for those keys, so we need to make sure this behavior is
|
||||
// replicated here.
|
||||
if coinType != 0 {
|
||||
return nil, fmt.Errorf("invalid BIP32 derivation path, coin " +
|
||||
"type must be 0 for BIP49/84 btcwallet keys")
|
||||
}
|
||||
|
||||
// We only expect to be asked to sign with key scopes that we know
|
||||
// about. So if the scope doesn't exist, we don't create it.
|
||||
scope := waddrmgr.KeyScope{
|
||||
Purpose: purpose,
|
||||
Coin: coinType,
|
||||
}
|
||||
scopedMgr, err := b.wallet.Manager.FetchScopedKeyManager(scope)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error fetching manager for scope %v: "+
|
||||
"%w", scope, err)
|
||||
}
|
||||
|
||||
// Let's see if we can hit the private key cache.
|
||||
keyPath := waddrmgr.DerivationPath{
|
||||
InternalAccount: account,
|
||||
Account: account,
|
||||
Branch: change,
|
||||
Index: index,
|
||||
}
|
||||
privKey, err := scopedMgr.DeriveFromKeyPathCache(keyPath)
|
||||
if err == nil {
|
||||
return privKey, nil
|
||||
}
|
||||
|
||||
// The key wasn't in the cache, let's fully derive it now.
|
||||
err = walletdb.View(b.db, func(tx walletdb.ReadTx) error {
|
||||
addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey)
|
||||
|
||||
addr, err := scopedMgr.DeriveFromKeyPath(addrmgrNs, keyPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error deriving private key: %w", err)
|
||||
}
|
||||
|
||||
privKey, err = addr.(waddrmgr.ManagedPubKeyAddress).PrivKey()
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error deriving key from path %#v: %w",
|
||||
keyPath, err)
|
||||
}
|
||||
|
||||
return privKey, nil
|
||||
}
|
||||
|
||||
// assertHardened makes sure each given element is >= 2^31.
|
||||
func assertHardened(elements ...uint32) error {
|
||||
for idx, element := range elements {
|
||||
if element < hdkeychain.HardenedKeyStart {
|
||||
return fmt.Errorf("element at index %d is not hardened",
|
||||
idx)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// deriveKeyByLocator attempts to derive a key stored in the wallet given a
|
||||
// valid key locator.
|
||||
func (b *BtcWallet) deriveKeyByLocator(
|
||||
|
241
lnwallet/btcwallet/signer_test.go
Normal file
241
lnwallet/btcwallet/signer_test.go
Normal file
@ -0,0 +1,241 @@
|
||||
package btcwallet
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/chaincfg"
|
||||
"github.com/btcsuite/btcd/integration/rpctest"
|
||||
"github.com/btcsuite/btcutil"
|
||||
"github.com/btcsuite/btcutil/hdkeychain"
|
||||
"github.com/btcsuite/btcwallet/chain"
|
||||
"github.com/lightningnetwork/lnd/blockcache"
|
||||
"github.com/lightningnetwork/lnd/lnwallet"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var (
|
||||
// seedBytes is the raw entropy of the aezeed:
|
||||
// able promote dizzy mixture sword myth share public find tattoo
|
||||
// catalog cousin bulb unfair machine alarm cool large promote kick
|
||||
// shop rug mean year
|
||||
// Which corresponds to the master root key:
|
||||
// xprv9s21ZrQH143K2KADjED57FvNbptdKLp4sqKzssegwEGKQMGoDkbyhUeCKe5m3A
|
||||
// MU44z4vqkmGswwQVKrv599nFG16PPZDEkNrogwoDGeCmZ
|
||||
seedBytes, _ = hex.DecodeString("4a7611b6979ba7c4bc5c5cd2239b2973")
|
||||
|
||||
// firstAddress is the first address that we should get from the wallet,
|
||||
// corresponding to the derivation path m/84'/0'/0'/0/0 (even on regtest
|
||||
// which is a special case for the BIP49/84 addresses in btcwallet).
|
||||
firstAddress = "bcrt1qgdlgjc5ede7fjv350wcjqat80m0zsmfaswsj9p"
|
||||
|
||||
testCases = []struct {
|
||||
name string
|
||||
path []uint32
|
||||
err string
|
||||
wif string
|
||||
}{{
|
||||
name: "m/84'/0'/0'/0/0",
|
||||
path: []uint32{
|
||||
hardenedKey(84), hardenedKey(0), hardenedKey(0), 0, 0,
|
||||
},
|
||||
wif: "cPp3XUewCBQVg3pgwVWbtpzDwhWTQpHhu8saN3SdGRTkiLpu1R6h",
|
||||
}, {
|
||||
name: "m/84'/0'/0'/1/0",
|
||||
path: []uint32{
|
||||
hardenedKey(84), hardenedKey(0), hardenedKey(0), 1, 0,
|
||||
},
|
||||
wif: "cPUR1nFAeYAtSWSkKoWB6WbzRTbDSGdrGRmv1kVLRPyo7QXph2gt",
|
||||
}, {
|
||||
name: "m/84'/0'/0'/0/12345",
|
||||
path: []uint32{
|
||||
hardenedKey(84), hardenedKey(0), hardenedKey(0), 0,
|
||||
12345,
|
||||
},
|
||||
wif: "cQCdGxqKeGZKiC2uRYMAGenJHkDvajiPieT4Yg7k1BKawjKkywvz",
|
||||
}, {
|
||||
name: "m/49'/0'/0'/0/0",
|
||||
path: []uint32{
|
||||
hardenedKey(49), hardenedKey(0), hardenedKey(0), 0, 0,
|
||||
},
|
||||
wif: "cMwVK2bcTzivPZfcCH585rBGghqsJAP9MdVy8inRti1wZvLn5DvY",
|
||||
}, {
|
||||
name: "m/49'/0'/0'/1/0",
|
||||
path: []uint32{
|
||||
hardenedKey(49), hardenedKey(0), hardenedKey(0), 1, 0,
|
||||
},
|
||||
wif: "cNPW9bMtdc2YGBzWzSCXFN4excjrT34nZzGYtfkzkazUrt3dXuv7",
|
||||
}, {
|
||||
name: "m/49'/0'/0'/1/12345",
|
||||
path: []uint32{
|
||||
hardenedKey(49), hardenedKey(0), hardenedKey(0), 1,
|
||||
12345,
|
||||
},
|
||||
wif: "cNdJt2fSNUJYVSb8JFjhosPcQgNvJ92SjNeNpsf1gUwDVDv2KVRa",
|
||||
}, {
|
||||
name: "m/1017'/1'/0'/0/0",
|
||||
path: []uint32{
|
||||
hardenedKey(1017), hardenedKey(1), hardenedKey(0), 0, 0,
|
||||
},
|
||||
wif: "cPsCmbWQENgptj3eTiyd85QSAD1xqYKPM9jUkfvm7vgN3SoVPWSP",
|
||||
}, {
|
||||
name: "m/1017'/1'/6'/0/0",
|
||||
path: []uint32{
|
||||
hardenedKey(1017), hardenedKey(1), hardenedKey(6), 0, 0,
|
||||
},
|
||||
wif: "cPeQdpcGJmLqpdmvokh3DK9ZtjYAXxiw4p4ELNUWkWt6bMRqArEV",
|
||||
}, {
|
||||
name: "m/1017'/1'/7'/0/123",
|
||||
path: []uint32{
|
||||
hardenedKey(1017), hardenedKey(1), hardenedKey(7), 0,
|
||||
123,
|
||||
},
|
||||
wif: "cPcWZMqY4YErkcwjtFJaYoXkzd7bKxrfxAVzhDgy3n5BGH8CU8sn",
|
||||
}, {
|
||||
name: "m/84'/1'/0'/0/0",
|
||||
path: []uint32{
|
||||
hardenedKey(84), hardenedKey(1), hardenedKey(0), 0, 0,
|
||||
},
|
||||
err: "coin type must be 0 for BIP49/84 btcwallet keys",
|
||||
}, {
|
||||
name: "m/1017'/0'/0'/0/0",
|
||||
path: []uint32{
|
||||
hardenedKey(1017), hardenedKey(0), hardenedKey(0), 0, 0,
|
||||
},
|
||||
err: "expected coin type 1, instead was 0",
|
||||
}, {
|
||||
name: "m/84'/0'/1'/0/0",
|
||||
path: []uint32{
|
||||
hardenedKey(84), hardenedKey(0), hardenedKey(1), 0, 0,
|
||||
},
|
||||
err: "account 1 not found",
|
||||
}, {
|
||||
name: "m/49'/0'/1'/0/0",
|
||||
path: []uint32{
|
||||
hardenedKey(49), hardenedKey(0), hardenedKey(1), 0, 0,
|
||||
},
|
||||
err: "account 1 not found",
|
||||
}, {
|
||||
name: "non-hardened purpose m/84/0/0/0/0",
|
||||
path: []uint32{84, 0, 0, 0, 0},
|
||||
err: "element at index 0 is not hardened",
|
||||
}, {
|
||||
name: "non-hardened account m/84'/0'/0/0/0",
|
||||
path: []uint32{hardenedKey(84), hardenedKey(0), 0, 0, 0},
|
||||
err: "element at index 2 is not hardened",
|
||||
}}
|
||||
)
|
||||
|
||||
// TestBip32KeyDerivation makes sure that private keys can be derived from a
|
||||
// BIP32 key path correctly.
|
||||
func TestBip32KeyDerivation(t *testing.T) {
|
||||
netParams := &chaincfg.RegressionNetParams
|
||||
w, cleanup := newTestWallet(t, netParams, seedBytes)
|
||||
defer cleanup()
|
||||
|
||||
// This is just a sanity check that the wallet was initialized
|
||||
// correctly. We make sure the first derived address is the expected
|
||||
// one.
|
||||
firstDerivedAddr, err := w.NewAddress(
|
||||
lnwallet.WitnessPubKey, false, lnwallet.DefaultAccountName,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, firstAddress, firstDerivedAddr.String())
|
||||
|
||||
// Let's go through the test cases now that we know our wallet is ready.
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
privKey, err := w.deriveKeyByBIP32Path(tc.path)
|
||||
|
||||
if tc.err == "" {
|
||||
require.NoError(t, err)
|
||||
wif, err := btcutil.NewWIF(
|
||||
privKey, netParams, true,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tc.wif, wif.String())
|
||||
} else {
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), tc.err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func newTestWallet(t *testing.T, netParams *chaincfg.Params,
|
||||
seedBytes []byte) (*BtcWallet, func()) {
|
||||
|
||||
tempDir, err := ioutil.TempDir("", "lnwallet")
|
||||
if err != nil {
|
||||
_ = os.RemoveAll(tempDir)
|
||||
t.Fatalf("creating temp dir failed: %v", err)
|
||||
}
|
||||
|
||||
chainBackend, backendCleanup := getChainBackend(t, netParams)
|
||||
cleanup := func() {
|
||||
_ = os.RemoveAll(tempDir)
|
||||
backendCleanup()
|
||||
}
|
||||
|
||||
loaderOpt := LoaderWithLocalWalletDB(tempDir, false, time.Minute)
|
||||
config := Config{
|
||||
PrivatePass: []byte("some-pass"),
|
||||
HdSeed: seedBytes,
|
||||
NetParams: netParams,
|
||||
CoinType: netParams.HDCoinType,
|
||||
ChainSource: chainBackend,
|
||||
// wallet starts in recovery mode
|
||||
RecoveryWindow: 2,
|
||||
LoaderOptions: []LoaderOption{loaderOpt},
|
||||
}
|
||||
blockCache := blockcache.NewBlockCache(10000)
|
||||
w, err := New(config, blockCache)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
t.Fatalf("creating wallet failed: %v", err)
|
||||
}
|
||||
|
||||
err = w.Start()
|
||||
if err != nil {
|
||||
cleanup()
|
||||
t.Fatalf("starting wallet failed: %v", err)
|
||||
}
|
||||
|
||||
return w, cleanup
|
||||
}
|
||||
|
||||
// getChainBackend returns a simple btcd based chain backend to back the wallet.
|
||||
func getChainBackend(t *testing.T, netParams *chaincfg.Params) (chain.Interface,
|
||||
func()) {
|
||||
|
||||
miningNode, err := rpctest.New(netParams, nil, nil, "")
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, miningNode.SetUp(true, 25))
|
||||
|
||||
// Next, mine enough blocks in order for SegWit and the CSV package
|
||||
// soft-fork to activate on RegNet.
|
||||
numBlocks := netParams.MinerConfirmationWindow * 2
|
||||
_, err = miningNode.Client.Generate(numBlocks)
|
||||
require.NoError(t, err)
|
||||
|
||||
rpcConfig := miningNode.RPCConfig()
|
||||
chainClient, err := chain.NewRPCClient(
|
||||
netParams, rpcConfig.Host, rpcConfig.User, rpcConfig.Pass,
|
||||
rpcConfig.Certificates, false, 20,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
return chainClient, func() {
|
||||
_ = miningNode.TearDown()
|
||||
}
|
||||
}
|
||||
|
||||
// hardenedKey returns a key of a hardened derivation key path.
|
||||
func hardenedKey(part uint32) uint32 {
|
||||
return part + hdkeychain.HardenedKeyStart
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user