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,
}
rootKey, _ = hdkeychain.NewMaster(seed, &chaincfg.MainNetParams)
pubPassphrase = []byte("_DJr{fL4H0O}*-0\n:V1izc)(6BomK")
privPassphrase = []byte("81lUHXnOMZ@?XXd7O9xyDIWIbXX-lj")
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
}
err = Create(
ns, seed, pubPassphrase, privPassphrase,
ns, rootKey, pubPassphrase, privPassphrase,
&chaincfg.MainNetParams, fastScrypt, time.Time{},
)
if err != nil {

View file

@ -1800,13 +1800,13 @@ func createManagerKeyScope(ns walletdb.ReadWriteBucket,
// A ManagerError with an error code of ErrAlreadyExists will be
// returned the address manager already exists in the specified
// namespace.
func Create(ns walletdb.ReadWriteBucket,
seed, pubPassphrase, privPassphrase []byte,
func Create(ns walletdb.ReadWriteBucket, rootKey *hdkeychain.ExtendedKey,
pubPassphrase, privPassphrase []byte,
chainParams *chaincfg.Params, config *ScryptOptions,
birthday time.Time) error {
// 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
// the given database namespace.
@ -1922,13 +1922,6 @@ func Create(ns walletdb.ReadWriteBucket,
// Generate the BIP0044 HD key structure to ensure the
// provided seed can generate the required structure with no
// 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()
if err != nil {
str := "failed to neuter master extended key"

View file

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

View file

@ -13,6 +13,7 @@ import (
"time"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcutil/hdkeychain"
"github.com/btcsuite/btcwallet/internal/prompt"
"github.com/btcsuite/btcwallet/waddrmgr"
"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,
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(
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,
seed []byte, bday time.Time, isWatchingOnly bool) (*Wallet, error) {
func (l *Loader) createNewWallet(pubPassphrase, privPassphrase []byte,
rootKey *hdkeychain.ExtendedKey, bday time.Time,
isWatchingOnly bool) (*Wallet, error) {
defer l.mu.Unlock()
l.mu.Lock()
@ -206,7 +242,7 @@ func (l *Loader) createNewWallet(pubPassphrase, privPassphrase,
}
} else {
err := CreateWithCallback(
l.db, pubPassphrase, privPassphrase, seed,
l.db, pubPassphrase, privPassphrase, rootKey,
l.chainParams, bday, l.walletCreated,
)
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
// called in the same transaction the wallet structure is initialized.
func CreateWithCallback(db walletdb.DB, pubPass, privPass, seed []byte,
params *chaincfg.Params, birthday time.Time,
cb func(walletdb.ReadWriteTx) error) error {
func CreateWithCallback(db walletdb.DB, pubPass, privPass []byte,
rootKey *hdkeychain.ExtendedKey, params *chaincfg.Params,
birthday time.Time, cb func(walletdb.ReadWriteTx) error) error {
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
// 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.
func Create(db walletdb.DB, pubPass, privPass, seed []byte,
params *chaincfg.Params, birthday time.Time) error {
func Create(db walletdb.DB, pubPass, privPass []byte,
rootKey *hdkeychain.ExtendedKey, params *chaincfg.Params,
birthday time.Time) error {
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
// 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
// either.
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,
params *chaincfg.Params, birthday time.Time, isWatchingOnly bool,
func create(db walletdb.DB, pubPass, privPass []byte,
rootKey *hdkeychain.ExtendedKey, params *chaincfg.Params,
birthday time.Time, isWatchingOnly bool,
cb func(walletdb.ReadWriteTx) error) error {
if !isWatchingOnly {
// If a seed was provided, ensure that it is of valid length. Otherwise,
// we generate a random seed for the wallet with the recommended seed
// length.
if seed == nil {
hdSeed, err := hdkeychain.GenerateSeed(
hdkeychain.RecommendedSeedLen)
if err != nil {
return err
}
seed = hdSeed
// If no root key was provided, we create one now from a random seed.
// But only if this is not a watching-only wallet where the accounts are
// created individually from their xpubs.
if !isWatchingOnly && rootKey == nil {
hdSeed, err := hdkeychain.GenerateSeed(
hdkeychain.RecommendedSeedLen,
)
if err != nil {
return err
}
if len(seed) < hdkeychain.MinSeedBytes ||
len(seed) > hdkeychain.MaxSeedBytes {
return hdkeychain.ErrInvalidSeedLen
// Derive the master extended key from the seed.
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 {
addrmgrNs, err := tx.CreateTopLevelBucket(waddrmgrNamespaceKey)
if err != nil {
@ -3763,7 +3772,8 @@ func create(db walletdb.DB, pubPass, privPass, seed []byte,
}
err = waddrmgr.Create(
addrmgrNs, seed, pubPass, privPass, params, nil, birthday,
addrmgrNs, rootKey, pubPass, privPass, params, nil,
birthday,
)
if err != nil {
return err