Merge pull request #720 from guggero/xprv-wallet-init

wallet: allow wallet to be created from extended master root key directly
This commit is contained in:
Olaoluwa Osuntokun 2021-08-22 15:29:49 -07:00 committed by GitHub
commit 9b5a201c34
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
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