wallet+waddrmgr: refactor to use extended key instead of seed

To allow a wallet to be created directly from an extended master root
key (xprv), we move the derivation from seed to extended key to the
loader instead of the address manager itself.
This commit is contained in:
Oliver Gugger 2020-09-26 12:14:09 +02:00 committed by Roy Lee
parent 6f4c9ce731
commit 4a75796117
5 changed files with 97 additions and 53 deletions

View file

@ -27,6 +27,8 @@ var (
0xb6, 0xc4, 0x40, 0xc0, 0x64, 0xb6, 0xc4, 0x40, 0xc0, 0x64,
} }
rootKey, _ = hdkeychain.NewMaster(seed, &chaincfg.MainNetParams)
pubPassphrase = []byte("_DJr{fL4H0O}*-0\n:V1izc)(6BomK") pubPassphrase = []byte("_DJr{fL4H0O}*-0\n:V1izc)(6BomK")
privPassphrase = []byte("81lUHXnOMZ@?XXd7O9xyDIWIbXX-lj") privPassphrase = []byte("81lUHXnOMZ@?XXd7O9xyDIWIbXX-lj")
pubPassphrase2 = []byte("-0NV4P~VSJBWbunw}%<Z]fuGpbN[ZI") pubPassphrase2 = []byte("-0NV4P~VSJBWbunw}%<Z]fuGpbN[ZI")
@ -285,7 +287,7 @@ func setupManager(t *testing.T) (tearDownFunc func(), db walletdb.DB, mgr *Manag
return err return err
} }
err = Create( err = Create(
ns, seed, pubPassphrase, privPassphrase, ns, rootKey, pubPassphrase, privPassphrase,
&chaincfg.MainNetParams, fastScrypt, time.Time{}, &chaincfg.MainNetParams, fastScrypt, time.Time{},
) )
if err != nil { if err != nil {

View file

@ -1800,13 +1800,13 @@ func createManagerKeyScope(ns walletdb.ReadWriteBucket,
// A ManagerError with an error code of ErrAlreadyExists will be // A ManagerError with an error code of ErrAlreadyExists will be
// returned the address manager already exists in the specified // returned the address manager already exists in the specified
// namespace. // namespace.
func Create(ns walletdb.ReadWriteBucket, func Create(ns walletdb.ReadWriteBucket, rootKey *hdkeychain.ExtendedKey,
seed, pubPassphrase, privPassphrase []byte, pubPassphrase, privPassphrase []byte,
chainParams *chaincfg.Params, config *ScryptOptions, chainParams *chaincfg.Params, config *ScryptOptions,
birthday time.Time) error { birthday time.Time) error {
// If the seed argument is nil we create in watchingOnly mode. // If the seed argument is nil we create in watchingOnly mode.
isWatchingOnly := seed == nil isWatchingOnly := rootKey == nil
// Return an error if the manager has already been created in // Return an error if the manager has already been created in
// the given database namespace. // the given database namespace.
@ -1922,13 +1922,6 @@ func Create(ns walletdb.ReadWriteBucket,
// Generate the BIP0044 HD key structure to ensure the // Generate the BIP0044 HD key structure to ensure the
// provided seed can generate the required structure with no // provided seed can generate the required structure with no
// issues. // issues.
// Derive the master extended key from the seed.
rootKey, err := hdkeychain.NewMaster(seed, chainParams)
if err != nil {
str := "failed to derive master extended key"
return managerError(ErrKeyChain, str, err)
}
rootPubKey, err := rootKey.Neuter() rootPubKey, err := rootKey.Neuter()
if err != nil { if err != nil {
str := "failed to neuter master extended key" str := "failed to neuter master extended key"

View file

@ -1816,32 +1816,35 @@ func TestManager(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
createdWatchingOnly bool createdWatchingOnly bool
seed []byte rootKey *hdkeychain.ExtendedKey
privPassphrase []byte privPassphrase []byte
}{ }{
{ {
name: "created with seed", name: "created with seed",
createdWatchingOnly: false, createdWatchingOnly: false,
seed: seed, rootKey: rootKey,
privPassphrase: privPassphrase, privPassphrase: privPassphrase,
}, },
{ {
name: "created watch-only", name: "created watch-only",
createdWatchingOnly: true, createdWatchingOnly: true,
seed: nil, rootKey: nil,
privPassphrase: nil, privPassphrase: nil,
}, },
} }
for _, test := range tests { for _, test := range tests {
// Need to wrap in a call so the defers work correctly. // Need to wrap in a call so the defers work correctly.
testManagerCase(t, test.name, test.createdWatchingOnly, testManagerCase(
test.seed, test.privPassphrase) t, test.name, test.createdWatchingOnly,
test.privPassphrase, test.rootKey,
)
} }
} }
func testManagerCase(t *testing.T, caseName string, func testManagerCase(t *testing.T, caseName string,
caseCreatedWatchingOnly bool, caseSeed, casePrivPassphrase []byte) { caseCreatedWatchingOnly bool, casePrivPassphrase []byte,
caseKey *hdkeychain.ExtendedKey) {
teardown, db := emptyDB(t) teardown, db := emptyDB(t)
defer teardown() defer teardown()
@ -1867,7 +1870,7 @@ func testManagerCase(t *testing.T, caseName string,
return err return err
} }
err = Create( err = Create(
ns, caseSeed, pubPassphrase, casePrivPassphrase, ns, caseKey, pubPassphrase, casePrivPassphrase,
&chaincfg.MainNetParams, fastScrypt, time.Time{}, &chaincfg.MainNetParams, fastScrypt, time.Time{},
) )
if err != nil { if err != nil {
@ -1897,7 +1900,7 @@ func testManagerCase(t *testing.T, caseName string,
err = walletdb.Update(db, func(tx walletdb.ReadWriteTx) error { err = walletdb.Update(db, func(tx walletdb.ReadWriteTx) error {
ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) ns := tx.ReadWriteBucket(waddrmgrNamespaceKey)
return Create( return Create(
ns, caseSeed, pubPassphrase, casePrivPassphrase, ns, caseKey, pubPassphrase, casePrivPassphrase,
&chaincfg.MainNetParams, fastScrypt, time.Time{}, &chaincfg.MainNetParams, fastScrypt, time.Time{},
) )
}) })
@ -2193,7 +2196,7 @@ func TestScopedKeyManagerManagement(t *testing.T) {
return err return err
} }
err = Create( err = Create(
ns, seed, pubPassphrase, privPassphrase, ns, rootKey, pubPassphrase, privPassphrase,
&chaincfg.MainNetParams, fastScrypt, time.Time{}, &chaincfg.MainNetParams, fastScrypt, time.Time{},
) )
if err != nil { if err != nil {
@ -2442,7 +2445,7 @@ func TestRootHDKeyNeutering(t *testing.T) {
return err return err
} }
err = Create( err = Create(
ns, seed, pubPassphrase, privPassphrase, ns, rootKey, pubPassphrase, privPassphrase,
&chaincfg.MainNetParams, fastScrypt, time.Time{}, &chaincfg.MainNetParams, fastScrypt, time.Time{},
) )
if err != nil { if err != nil {
@ -2534,7 +2537,7 @@ func TestNewRawAccount(t *testing.T) {
return err return err
} }
err = Create( err = Create(
ns, seed, pubPassphrase, privPassphrase, ns, rootKey, pubPassphrase, privPassphrase,
&chaincfg.MainNetParams, fastScrypt, time.Time{}, &chaincfg.MainNetParams, fastScrypt, time.Time{},
) )
if err != nil { if err != nil {
@ -2660,7 +2663,7 @@ func TestNewRawAccountHybrid(t *testing.T) {
return err return err
} }
err = Create( err = Create(
ns, seed, pubPassphrase, privPassphrase, ns, rootKey, pubPassphrase, privPassphrase,
&chaincfg.MainNetParams, fastScrypt, time.Time{}, &chaincfg.MainNetParams, fastScrypt, time.Time{},
) )
if err != nil { if err != nil {

View file

@ -13,6 +13,7 @@ import (
"time" "time"
"github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcutil/hdkeychain"
"github.com/btcsuite/btcwallet/internal/prompt" "github.com/btcsuite/btcwallet/internal/prompt"
"github.com/btcsuite/btcwallet/waddrmgr" "github.com/btcsuite/btcwallet/waddrmgr"
"github.com/btcsuite/btcwallet/walletdb" "github.com/btcsuite/btcwallet/walletdb"
@ -145,8 +146,42 @@ func (l *Loader) OnWalletCreated(fn func(walletdb.ReadWriteTx) error) {
func (l *Loader) CreateNewWallet(pubPassphrase, privPassphrase, seed []byte, func (l *Loader) CreateNewWallet(pubPassphrase, privPassphrase, seed []byte,
bday time.Time) (*Wallet, error) { bday time.Time) (*Wallet, error) {
var (
rootKey *hdkeychain.ExtendedKey
err error
)
// If a seed was specified, we check its length now. If no seed is
// passed, the wallet will create a new random one.
if seed != nil {
if len(seed) < hdkeychain.MinSeedBytes ||
len(seed) > hdkeychain.MaxSeedBytes {
return nil, hdkeychain.ErrInvalidSeedLen
}
// Derive the master extended key from the seed.
rootKey, err = hdkeychain.NewMaster(seed, l.chainParams)
if err != nil {
return nil, fmt.Errorf("failed to derive master " +
"extended key")
}
}
return l.createNewWallet( return l.createNewWallet(
pubPassphrase, privPassphrase, seed, bday, false, pubPassphrase, privPassphrase, rootKey, bday, false,
)
}
// CreateNewWalletExtendedKey creates a new wallet from an extended master root
// key using the provided public and private passphrases. The root key is
// optional. If non-nil, addresses are derived from this root key. If nil, a
// secure random seed is generated and the root key is derived from that.
func (l *Loader) CreateNewWalletExtendedKey(pubPassphrase, privPassphrase []byte,
rootKey *hdkeychain.ExtendedKey, bday time.Time) (*Wallet, error) {
return l.createNewWallet(
pubPassphrase, privPassphrase, rootKey, bday, false,
) )
} }
@ -161,8 +196,9 @@ func (l *Loader) CreateNewWatchingOnlyWallet(pubPassphrase []byte,
) )
} }
func (l *Loader) createNewWallet(pubPassphrase, privPassphrase, func (l *Loader) createNewWallet(pubPassphrase, privPassphrase []byte,
seed []byte, bday time.Time, isWatchingOnly bool) (*Wallet, error) { rootKey *hdkeychain.ExtendedKey, bday time.Time,
isWatchingOnly bool) (*Wallet, error) {
defer l.mu.Unlock() defer l.mu.Unlock()
l.mu.Lock() l.mu.Lock()
@ -206,7 +242,7 @@ func (l *Loader) createNewWallet(pubPassphrase, privPassphrase,
} }
} else { } else {
err := CreateWithCallback( err := CreateWithCallback(
l.db, pubPassphrase, privPassphrase, seed, l.db, pubPassphrase, privPassphrase, rootKey,
l.chainParams, bday, l.walletCreated, l.chainParams, bday, l.walletCreated,
) )
if err != nil { if err != nil {

View file

@ -3686,12 +3686,12 @@ func (w *Wallet) Database() walletdb.DB {
// CreateWithCallback is the same as Create with an added callback that will be // CreateWithCallback is the same as Create with an added callback that will be
// called in the same transaction the wallet structure is initialized. // called in the same transaction the wallet structure is initialized.
func CreateWithCallback(db walletdb.DB, pubPass, privPass, seed []byte, func CreateWithCallback(db walletdb.DB, pubPass, privPass []byte,
params *chaincfg.Params, birthday time.Time, rootKey *hdkeychain.ExtendedKey, params *chaincfg.Params,
cb func(walletdb.ReadWriteTx) error) error { birthday time.Time, cb func(walletdb.ReadWriteTx) error) error {
return create( return create(
db, pubPass, privPass, seed, params, birthday, false, cb, db, pubPass, privPass, rootKey, params, birthday, false, cb,
) )
} }
@ -3708,18 +3708,19 @@ func CreateWatchingOnlyWithCallback(db walletdb.DB, pubPass []byte,
} }
// Create creates an new wallet, writing it to an empty database. If the passed // Create creates an new wallet, writing it to an empty database. If the passed
// seed is non-nil, it is used. Otherwise, a secure random seed of the // root key is non-nil, it is used. Otherwise, a secure random seed of the
// recommended length is generated. // recommended length is generated.
func Create(db walletdb.DB, pubPass, privPass, seed []byte, func Create(db walletdb.DB, pubPass, privPass []byte,
params *chaincfg.Params, birthday time.Time) error { rootKey *hdkeychain.ExtendedKey, params *chaincfg.Params,
birthday time.Time) error {
return create( return create(
db, pubPass, privPass, seed, params, birthday, false, nil, db, pubPass, privPass, rootKey, params, birthday, false, nil,
) )
} }
// CreateWatchingOnly creates an new watch-only wallet, writing it to // CreateWatchingOnly creates an new watch-only wallet, writing it to
// an empty database. No seed can be provided as this wallet will be // an empty database. No root key can be provided as this wallet will be
// watching only. Likewise no private passphrase may be provided // watching only. Likewise no private passphrase may be provided
// either. // either.
func CreateWatchingOnly(db walletdb.DB, pubPass []byte, func CreateWatchingOnly(db walletdb.DB, pubPass []byte,
@ -3730,28 +3731,36 @@ func CreateWatchingOnly(db walletdb.DB, pubPass []byte,
) )
} }
func create(db walletdb.DB, pubPass, privPass, seed []byte, func create(db walletdb.DB, pubPass, privPass []byte,
params *chaincfg.Params, birthday time.Time, isWatchingOnly bool, rootKey *hdkeychain.ExtendedKey, params *chaincfg.Params,
birthday time.Time, isWatchingOnly bool,
cb func(walletdb.ReadWriteTx) error) error { cb func(walletdb.ReadWriteTx) error) error {
if !isWatchingOnly { // If no root key was provided, we create one now from a random seed.
// If a seed was provided, ensure that it is of valid length. Otherwise, // But only if this is not a watching-only wallet where the accounts are
// we generate a random seed for the wallet with the recommended seed // created individually from their xpubs.
// length. if !isWatchingOnly && rootKey == nil {
if seed == nil { hdSeed, err := hdkeychain.GenerateSeed(
hdSeed, err := hdkeychain.GenerateSeed( hdkeychain.RecommendedSeedLen,
hdkeychain.RecommendedSeedLen) )
if err != nil { if err != nil {
return err return err
}
seed = hdSeed
} }
if len(seed) < hdkeychain.MinSeedBytes ||
len(seed) > hdkeychain.MaxSeedBytes { // Derive the master extended key from the seed.
return hdkeychain.ErrInvalidSeedLen rootKey, err = hdkeychain.NewMaster(hdSeed, params)
if err != nil {
return fmt.Errorf("failed to derive master extended " +
"key")
} }
} }
// We need a private key if this isn't a watching only wallet.
if !isWatchingOnly && rootKey != nil && !rootKey.IsPrivate() {
return fmt.Errorf("need extended private key for wallet that " +
"is not watching only")
}
return walletdb.Update(db, func(tx walletdb.ReadWriteTx) error { return walletdb.Update(db, func(tx walletdb.ReadWriteTx) error {
addrmgrNs, err := tx.CreateTopLevelBucket(waddrmgrNamespaceKey) addrmgrNs, err := tx.CreateTopLevelBucket(waddrmgrNamespaceKey)
if err != nil { if err != nil {
@ -3763,7 +3772,8 @@ func create(db walletdb.DB, pubPass, privPass, seed []byte,
} }
err = waddrmgr.Create( err = waddrmgr.Create(
addrmgrNs, seed, pubPass, privPass, params, nil, birthday, addrmgrNs, rootKey, pubPass, privPass, params, nil,
birthday,
) )
if err != nil { if err != nil {
return err return err