From 4c5bc1b15d3c0dc0d070ca019f4653ff95657452 Mon Sep 17 00:00:00 2001 From: Dev Random Date: Fri, 24 Apr 2020 17:44:21 -0700 Subject: [PATCH] waddrmgr: create watch-only address managers and accounts This PR allows the creation of managers and accounts that are watch-only. The state of the database after creation would be identical to the state after calling Manager.ConvertToWatchingOnly, assuming accounts with the right xpubs were created in the former case. Co-authored-by: Ken Sedgwick --- waddrmgr/README.md | 3 +- waddrmgr/db.go | 4 + waddrmgr/manager.go | 335 ++++++++++++++++------------- waddrmgr/manager_test.go | 412 +++++++++++++++++++++++++++++------- waddrmgr/scoped_manager.go | 85 +++++++- wallet/loader.go | 36 +++- wallet/wallet.go | 55 +++-- wallet/watchingonly_test.go | 34 +++ 8 files changed, 720 insertions(+), 244 deletions(-) create mode 100644 wallet/watchingonly_test.go diff --git a/waddrmgr/README.md b/waddrmgr/README.md index 6499c6d..ae39edc 100644 --- a/waddrmgr/README.md +++ b/waddrmgr/README.md @@ -32,8 +32,9 @@ report. Package waddrmgr is licensed under the liberal ISC license. - Import WIF keys - Import pay-to-script-hash scripts for things such as multi-signature transactions - - Ability to export a watching-only version which does not contain any private + - Ability to start in watching-only mode which does not contain any private key material + - Ability to convert to watching-only mode - Programmatically detectable errors, including encapsulation of errors from packages it relies on - Address synchronization capabilities diff --git a/waddrmgr/db.go b/waddrmgr/db.go index 97ecc5a..19167ea 100644 --- a/waddrmgr/db.go +++ b/waddrmgr/db.go @@ -850,6 +850,7 @@ func forEachAccount(ns walletdb.ReadBucket, scope *KeyScope, } // fetchLastAccount retrieves the last account from the database. +// If no accounts, returns twos-complement representation of -1, so that the next account is zero func fetchLastAccount(ns walletdb.ReadBucket, scope *KeyScope) (uint32, error) { scopedBucket, err := fetchReadScopeBucket(ns, scope) if err != nil { @@ -859,6 +860,9 @@ func fetchLastAccount(ns walletdb.ReadBucket, scope *KeyScope) (uint32, error) { metaBucket := scopedBucket.NestedReadBucket(metaBucketName) val := metaBucket.Get(lastAccountName) + if val == nil { + return (1 << 32) - 1, nil + } if len(val) != 4 { str := fmt.Sprintf("malformed metadata '%s' stored in database", lastAccountName) diff --git a/waddrmgr/manager.go b/waddrmgr/manager.go index 8dc279a..b0f3bee 100644 --- a/waddrmgr/manager.go +++ b/waddrmgr/manager.go @@ -438,53 +438,61 @@ func (m *Manager) Close() { // // TODO(roasbeef): addrtype of raw key means it'll look in scripts to possibly // mark as gucci? -func (m *Manager) NewScopedKeyManager(ns walletdb.ReadWriteBucket, scope KeyScope, - addrSchema ScopeAddrSchema) (*ScopedKeyManager, error) { +func (m *Manager) NewScopedKeyManager(ns walletdb.ReadWriteBucket, + scope KeyScope, addrSchema ScopeAddrSchema) (*ScopedKeyManager, error) { m.mtx.Lock() defer m.mtx.Unlock() - // If the manager is locked, then we can't create a new scoped manager. - if m.locked { - return nil, managerError(ErrLocked, errLocked, nil) - } + var rootPriv *hdkeychain.ExtendedKey + if !m.watchingOnly { + // If the manager is locked, then we can't create a new scoped + // manager. + if m.locked { + return nil, managerError(ErrLocked, errLocked, nil) + } - // Now that we know the manager is unlocked, we'll need to fetch the - // root master HD private key. This is required as we'll be attempting - // the following derivation: m/purpose'/cointype' - // - // Note that the path to the coin type is requires hardened derivation, - // therefore this can only be done if the wallet's root key hasn't been - // neutered. - masterRootPrivEnc, _, err := fetchMasterHDKeys(ns) - if err != nil { - return nil, err - } + // Now that we know the manager is unlocked, we'll need to + // fetch the root master HD private key. This is required as + // we'll be attempting the following derivation: + // m/purpose'/cointype' + // + // Note that the path to the coin type is requires hardened + // derivation, therefore this can only be done if the wallet's + // root key hasn't been neutered. + masterRootPrivEnc, _, err := fetchMasterHDKeys(ns) + if err != nil { + return nil, err + } - // If the master root private key isn't found within the database, but - // we need to bail here as we can't create the cointype key without the - // master root private key. - if masterRootPrivEnc == nil { - return nil, managerError(ErrWatchingOnly, "", nil) - } + // If the master root private key isn't found within the + // database, but we need to bail here as we can't create the + // cointype key without the master root private key. + if masterRootPrivEnc == nil { + return nil, managerError(ErrWatchingOnly, "", nil) + } - // Before we can derive any new scoped managers using this key, we'll - // need to fully decrypt it. - serializedMasterRootPriv, err := m.cryptoKeyPriv.Decrypt(masterRootPrivEnc) - if err != nil { - str := fmt.Sprintf("failed to decrypt master root serialized private key") - return nil, managerError(ErrLocked, str, err) - } + // Before we can derive any new scoped managers using this + // key, we'll need to fully decrypt it. + serializedMasterRootPriv, err := + m.cryptoKeyPriv.Decrypt(masterRootPrivEnc) + if err != nil { + str := fmt.Sprintf("failed to decrypt master root " + + "serialized private key") + return nil, managerError(ErrLocked, str, err) + } - // Now that we know the root priv is within the database, we'll decode - // it into a usable object. - rootPriv, err := hdkeychain.NewKeyFromString( - string(serializedMasterRootPriv), - ) - zero.Bytes(serializedMasterRootPriv) - if err != nil { - str := fmt.Sprintf("failed to create master extended private key") - return nil, managerError(ErrKeyChain, str, err) + // Now that we know the root priv is within the database, + // we'll decode it into a usable object. + rootPriv, err = hdkeychain.NewKeyFromString( + string(serializedMasterRootPriv), + ) + zero.Bytes(serializedMasterRootPriv) + if err != nil { + str := fmt.Sprintf("failed to create master extended " + + "private key") + return nil, managerError(ErrKeyChain, str, err) + } } // Now that we have the root private key, we'll fetch the scope bucket @@ -506,19 +514,21 @@ func (m *Manager) NewScopedKeyManager(ns walletdb.ReadWriteBucket, scope KeyScop } scopeKey := scopeToBytes(&scope) schemaBytes := scopeSchemaToBytes(&addrSchema) - err = scopeSchemas.Put(scopeKey[:], schemaBytes) + err := scopeSchemas.Put(scopeKey[:], schemaBytes) if err != nil { return nil, err } - // With the database state created, we'll now derive the cointype key - // using the master HD private key, then encrypt it along with the - // first account using our crypto keys. - err = createManagerKeyScope( - ns, scope, rootPriv, m.cryptoKeyPub, m.cryptoKeyPriv, - ) - if err != nil { - return nil, err + if !m.watchingOnly { + // With the database state created, we'll now derive the + // cointype key using the master HD private key, then encrypt + // it along with the first account using our crypto keys. + err = createManagerKeyScope( + ns, scope, rootPriv, m.cryptoKeyPub, m.cryptoKeyPriv, + ) + if err != nil { + return nil, err + } } // Finally, we'll register this new scoped manager with the root @@ -1298,7 +1308,7 @@ func newManager(chainParams *chaincfg.Params, masterKeyPub *snacl.SecretKey, masterKeyPriv *snacl.SecretKey, cryptoKeyPub EncryptorDecryptor, cryptoKeyPrivEncrypted, cryptoKeyScriptEncrypted []byte, syncInfo *syncState, birthday time.Time, privPassphraseSalt [saltSize]byte, - scopedManagers map[KeyScope]*ScopedKeyManager) *Manager { + scopedManagers map[KeyScope]*ScopedKeyManager, watchingOnly bool) *Manager { m := &Manager{ chainParams: chainParams, @@ -1316,6 +1326,7 @@ func newManager(chainParams *chaincfg.Params, masterKeyPub *snacl.SecretKey, scopedManagers: scopedManagers, externalAddrSchemas: make(map[AddressType][]KeyScope), internalAddrSchemas: make(map[AddressType][]KeyScope), + watchingOnly: watchingOnly, } for _, sMgr := range m.scopedManagers { @@ -1540,9 +1551,8 @@ func loadManager(ns walletdb.ReadBucket, pubPassphrase []byte, mgr := newManager( chainParams, &masterKeyPub, &masterKeyPriv, cryptoKeyPub, cryptoKeyPrivEnc, cryptoKeyScriptEnc, syncInfo, - birthday, privPassphraseSalt, scopedManagers, + birthday, privPassphraseSalt, scopedManagers, watchingOnly, ) - mgr.watchingOnly = watchingOnly for _, scopedManager := range scopedManagers { scopedManager.rootManager = mgr @@ -1676,27 +1686,40 @@ func createManagerKeyScope(ns walletdb.ReadWriteBucket, ) } -// Create creates a new address manager in the given namespace. The seed must -// conform to the standards described in hdkeychain.NewMaster and will be used -// to create the master root node from which all hierarchical deterministic -// addresses are derived. This allows all chained addresses in the address -// manager to be recovered by using the same seed. +// Create creates a new address manager in the given namespace. // -// All private and public keys and information are protected by secret keys -// derived from the provided private and public passphrases. The public -// passphrase is required on subsequent opens of the address manager, and the -// private passphrase is required to unlock the address manager in order to -// gain access to any private keys and information. +// The seed must conform to the standards described in +// hdkeychain.NewMaster and will be used to create the master root +// node from which all hierarchical deterministic addresses are +// derived. This allows all chained addresses in the address manager +// to be recovered by using the same seed. // -// If a config structure is passed to the function, that configuration will -// override the defaults. +// If the provided seed value is nil the address manager will be +// created in watchingOnly mode in which case no default accounts or +// scoped managers are created - it is up to the caller to create a +// new one with NewAccountWatchingOnly and NewScopedKeyManager. // -// 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, +// All private and public keys and information are protected by secret +// keys derived from the provided private and public passphrases. The +// public passphrase is required on subsequent opens of the address +// manager, and the private passphrase is required to unlock the +// address manager in order to gain access to any private keys and +// information. +// +// If a config structure is passed to the function, that configuration +// will override the defaults. +// +// 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, chainParams *chaincfg.Params, config *ScryptOptions, birthday time.Time) error { + // If the seed argument is nil we create in watchingOnly mode. + isWatchingOnly := seed == nil + // Return an error if the manager has already been created in // the given database namespace. exists := managerExists(ns) @@ -1705,13 +1728,17 @@ func Create(ns walletdb.ReadWriteBucket, seed, pubPassphrase, privPassphrase []b } // Ensure the private passphrase is not empty. - if len(privPassphrase) == 0 { + if !isWatchingOnly && len(privPassphrase) == 0 { str := "private passphrase may not be empty" return managerError(ErrEmptyPassphrase, str, nil) } // Perform the initial bucket creation and database namespace setup. - if err := createManagerNS(ns, ScopeAddrMap); err != nil { + defaultScopes := map[KeyScope]ScopeAddrSchema{} + if !isWatchingOnly { + defaultScopes = ScopeAddrMap + } + if err := createManagerNS(ns, defaultScopes); err != nil { return maybeConvertDbError(err) } @@ -1726,22 +1753,6 @@ func Create(ns walletdb.ReadWriteBucket, seed, pubPassphrase, privPassphrase []b str := "failed to master public key" return managerError(ErrCrypto, str, err) } - masterKeyPriv, err := newSecretKey(&privPassphrase, config) - if err != nil { - str := "failed to master private key" - return managerError(ErrCrypto, str, err) - } - defer masterKeyPriv.Zero() - - // Generate the private passphrase salt. This is used when hashing - // passwords to detect whether an unlock can be avoided when the manager - // is already unlocked. - var privPassphraseSalt [saltSize]byte - _, err = rand.Read(privPassphraseSalt[:]) - if err != nil { - str := "failed to read random source for passphrase salt" - return managerError(ErrCrypto, str, err) - } // Generate new crypto public, private, and script keys. These keys are // used to protect the actual public and private data such as addresses, @@ -1751,18 +1762,6 @@ func Create(ns walletdb.ReadWriteBucket, seed, pubPassphrase, privPassphrase []b str := "failed to generate crypto public key" return managerError(ErrCrypto, str, err) } - cryptoKeyPriv, err := newCryptoKey() - if err != nil { - str := "failed to generate crypto private key" - return managerError(ErrCrypto, str, err) - } - defer cryptoKeyPriv.Zero() - cryptoKeyScript, err := newCryptoKey() - if err != nil { - str := "failed to generate crypto script key" - return managerError(ErrCrypto, str, err) - } - defer cryptoKeyScript.Zero() // Encrypt the crypto keys with the associated master keys. cryptoKeyPubEnc, err := masterKeyPub.Encrypt(cryptoKeyPub.Bytes()) @@ -1770,16 +1769,6 @@ func Create(ns walletdb.ReadWriteBucket, seed, pubPassphrase, privPassphrase []b str := "failed to encrypt crypto public key" return managerError(ErrCrypto, str, err) } - cryptoKeyPrivEnc, err := masterKeyPriv.Encrypt(cryptoKeyPriv.Bytes()) - if err != nil { - str := "failed to encrypt crypto private key" - return managerError(ErrCrypto, str, err) - } - cryptoKeyScriptEnc, err := masterKeyPriv.Encrypt(cryptoKeyScript.Bytes()) - if err != nil { - str := "failed to encrypt crypto script key" - return managerError(ErrCrypto, str, err) - } // Use the genesis block for the passed chain as the created at block // for the default. @@ -1788,52 +1777,108 @@ func Create(ns walletdb.ReadWriteBucket, seed, pubPassphrase, privPassphrase []b // Create the initial sync state. syncInfo := newSyncState(createdAt, createdAt) - // Save the master key params to the database. pubParams := masterKeyPub.Marshal() - privParams := masterKeyPriv.Marshal() - err = putMasterKeyParams(ns, pubParams, privParams) - if err != nil { - return maybeConvertDbError(err) - } - // Generate the BIP0044 HD key structure to ensure the provided seed - // can generate the required structure with no issues. + var privParams []byte = nil + var masterKeyPriv *snacl.SecretKey + var cryptoKeyPrivEnc []byte = nil + var cryptoKeyScriptEnc []byte = nil + if !isWatchingOnly { + masterKeyPriv, err = newSecretKey(&privPassphrase, config) + if err != nil { + str := "failed to master private key" + return managerError(ErrCrypto, str, err) + } + defer masterKeyPriv.Zero() - // 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" - return managerError(ErrKeyChain, str, err) - } + // Generate the private passphrase salt. This is used when + // hashing passwords to detect whether an unlock can be + // avoided when the manager is already unlocked. + var privPassphraseSalt [saltSize]byte + _, err = rand.Read(privPassphraseSalt[:]) + if err != nil { + str := "failed to read random source for passphrase salt" + return managerError(ErrCrypto, str, err) + } - // Next, for each registers default manager scope, we'll create the - // hardened cointype key for it, as well as the first default account. - for _, defaultScope := range DefaultKeyScopes { - err := createManagerKeyScope( - ns, defaultScope, rootKey, cryptoKeyPub, cryptoKeyPriv, - ) + cryptoKeyPriv, err := newCryptoKey() + if err != nil { + str := "failed to generate crypto private key" + return managerError(ErrCrypto, str, err) + } + defer cryptoKeyPriv.Zero() + cryptoKeyScript, err := newCryptoKey() + if err != nil { + str := "failed to generate crypto script key" + return managerError(ErrCrypto, str, err) + } + defer cryptoKeyScript.Zero() + + cryptoKeyPrivEnc, err = + masterKeyPriv.Encrypt(cryptoKeyPriv.Bytes()) + if err != nil { + str := "failed to encrypt crypto private key" + return managerError(ErrCrypto, str, err) + } + cryptoKeyScriptEnc, err = + masterKeyPriv.Encrypt(cryptoKeyScript.Bytes()) + if err != nil { + str := "failed to encrypt crypto script key" + return managerError(ErrCrypto, str, err) + } + + // 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" + return managerError(ErrKeyChain, str, err) + } + + // Next, for each registers default manager scope, we'll + // create the hardened cointype key for it, as well as the + // first default account. + for _, defaultScope := range DefaultKeyScopes { + err := createManagerKeyScope( + ns, defaultScope, rootKey, cryptoKeyPub, cryptoKeyPriv, + ) + if err != nil { + return maybeConvertDbError(err) + } + } + + // Before we proceed, we'll also store the root master private + // key within the database in an encrypted format. This is + // required as in the future, we may need to create additional + // scoped key managers. + masterHDPrivKeyEnc, err := + cryptoKeyPriv.Encrypt([]byte(rootKey.String())) if err != nil { return maybeConvertDbError(err) } + masterHDPubKeyEnc, err := + cryptoKeyPub.Encrypt([]byte(rootPubKey.String())) + if err != nil { + return maybeConvertDbError(err) + } + err = putMasterHDKeys(ns, masterHDPrivKeyEnc, masterHDPubKeyEnc) + if err != nil { + return maybeConvertDbError(err) + } + + privParams = masterKeyPriv.Marshal() } - // Before we proceed, we'll also store the root master private key - // within the database in an encrypted format. This is required as in - // the future, we may need to create additional scoped key managers. - masterHDPrivKeyEnc, err := cryptoKeyPriv.Encrypt([]byte(rootKey.String())) - if err != nil { - return maybeConvertDbError(err) - } - masterHDPubKeyEnc, err := cryptoKeyPub.Encrypt([]byte(rootPubKey.String())) - if err != nil { - return maybeConvertDbError(err) - } - err = putMasterHDKeys(ns, masterHDPrivKeyEnc, masterHDPubKeyEnc) + // Save the master key params to the database. + err = putMasterKeyParams(ns, pubParams, privParams) if err != nil { return maybeConvertDbError(err) } @@ -1845,9 +1890,9 @@ func Create(ns walletdb.ReadWriteBucket, seed, pubPassphrase, privPassphrase []b return maybeConvertDbError(err) } - // Save the fact this is not a watching-only address manager to the + // Save the watching-only mode of the address manager to the // database. - err = putWatchingOnly(ns, false) + err = putWatchingOnly(ns, isWatchingOnly) if err != nil { return maybeConvertDbError(err) } diff --git a/waddrmgr/manager_test.go b/waddrmgr/manager_test.go index cb80a7d..0db0a88 100644 --- a/waddrmgr/manager_test.go +++ b/waddrmgr/manager_test.go @@ -17,6 +17,7 @@ import ( "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcutil" + "github.com/btcsuite/btcutil/hdkeychain" "github.com/btcsuite/btcwallet/snacl" "github.com/btcsuite/btcwallet/walletdb" "github.com/davecgh/go-spew/spew" @@ -71,6 +72,7 @@ func failingSecretKeyGen(passphrase *[]byte, // spent. type testContext struct { t *testing.T + caseName string db walletdb.DB rootManager *Manager manager *ScopedKeyManager @@ -112,7 +114,7 @@ func testNamePrefix(tc *testContext) string { prefix = "Create " } - return prefix + fmt.Sprintf("account #%d", tc.account) + return fmt.Sprintf("(%s) %s account #%d", tc.caseName, prefix, tc.account) } // testManagedPubKeyAddress ensures the data returned by all exported functions @@ -1049,7 +1051,7 @@ func testImportScript(tc *testContext) bool { } // testMarkUsed ensures used addresses are flagged as such. -func testMarkUsed(tc *testContext) bool { +func testMarkUsed(tc *testContext, doScript bool) bool { tests := []struct { name string typ addrType @@ -1067,9 +1069,12 @@ func testMarkUsed(tc *testContext) bool { }, } - prefix := "MarkUsed" + prefix := fmt.Sprintf("(%s) MarkUsed", tc.caseName) chainParams := tc.manager.ChainParams() for i, test := range tests { + if !doScript && test.typ == addrScriptHash { + continue + } addrHash := test.in var addr btcutil.Address @@ -1116,7 +1121,7 @@ func testMarkUsed(tc *testContext) bool { return nil }) if err != nil { - tc.t.Errorf("Unexpected error %v", err) + tc.t.Errorf("(%s) Unexpected error %v", tc.caseName, err) } } @@ -1126,10 +1131,12 @@ func testMarkUsed(tc *testContext) bool { // testChangePassphrase ensures changes both the public and private passphrases // works as intended. func testChangePassphrase(tc *testContext) bool { + pfx := fmt.Sprintf("(%s) ", tc.caseName) + // Force an error when changing the passphrase due to failure to // generate a new secret key by replacing the generation function one // that intentionally errors. - testName := "ChangePassphrase (public) with invalid new secret key" + testName := pfx + "ChangePassphrase (public) with invalid new secret key" oldKeyGen := SetSecretKeyGen(failingSecretKeyGen) err := walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) error { @@ -1143,7 +1150,7 @@ func testChangePassphrase(tc *testContext) bool { } // Attempt to change public passphrase with invalid old passphrase. - testName = "ChangePassphrase (public) with invalid old passphrase" + testName = pfx + "ChangePassphrase (public) with invalid old passphrase" SetSecretKeyGen(oldKeyGen) err = walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) error { ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) @@ -1156,7 +1163,7 @@ func testChangePassphrase(tc *testContext) bool { } // Change the public passphrase. - testName = "ChangePassphrase (public)" + testName = pfx + "ChangePassphrase (public)" err = walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) error { ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) return tc.rootManager.ChangePassphrase( @@ -1192,7 +1199,7 @@ func testChangePassphrase(tc *testContext) bool { // Attempt to change private passphrase with invalid old passphrase. // The error should be ErrWrongPassphrase or ErrWatchingOnly depending // on the type of the address manager. - testName = "ChangePassphrase (private) with invalid old passphrase" + testName = pfx + "ChangePassphrase (private) with invalid old passphrase" err = walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) error { ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) return tc.rootManager.ChangePassphrase( @@ -1216,7 +1223,7 @@ func testChangePassphrase(tc *testContext) bool { } // Change the private passphrase. - testName = "ChangePassphrase (private)" + testName = pfx + "ChangePassphrase (private)" err = walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) error { ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) return tc.rootManager.ChangePassphrase( @@ -1379,6 +1386,18 @@ func testLookupAccount(tc *testContext) bool { defaultAccountName: DefaultAccountNum, ImportedAddrAccountName: ImportedAddrAccount, } + + var expectedLastAccount uint32 = 1 + if !tc.create { + // Existing wallet manager will have 3 accounts + expectedLastAccount = 2 + } + + return testLookupExpectedAccount(tc, expectedAccounts, expectedLastAccount) +} + +func testLookupExpectedAccount(tc *testContext, expectedAccounts map[string]uint32, + expectedLastAccount uint32) bool { for acctName, expectedAccount := range expectedAccounts { var account uint32 err := walletdb.View(tc.db, func(tx walletdb.ReadTx) error { @@ -1418,19 +1437,12 @@ func testLookupAccount(tc *testContext) bool { lastAccount, err = tc.manager.LastAccount(ns) return err }) - var expectedLastAccount uint32 - expectedLastAccount = 1 - if !tc.create { - // Existing wallet manager will have 3 accounts - expectedLastAccount = 2 - } if lastAccount != expectedLastAccount { tc.t.Errorf("LookupAccount "+ "account mismatch -- got %d, "+ "want %d", lastAccount, expectedLastAccount) return false } - // Test account lookup for default account adddress var expectedAccount uint32 for i, addr := range expectedAddrs { @@ -1608,31 +1620,51 @@ func testForEachAccountAddress(tc *testContext) bool { // testManagerAPI tests the functions provided by the Manager API as well as // the ManagedAddress, ManagedPubKeyAddress, and ManagedScriptAddress // interfaces. -func testManagerAPI(tc *testContext) { - testLocking(tc) - testExternalAddresses(tc) - testInternalAddresses(tc) - testImportPrivateKey(tc) - testImportScript(tc) - testMarkUsed(tc) - testChangePassphrase(tc) +func testManagerAPI(tc *testContext, caseCreatedWatchingOnly bool) { + if !caseCreatedWatchingOnly { + // Test API for normal create (w/ seed) case. + testLocking(tc) + testExternalAddresses(tc) + testInternalAddresses(tc) + testImportPrivateKey(tc) + testImportScript(tc) + testMarkUsed(tc, true) + testChangePassphrase(tc) - // Reset default account - tc.account = 0 - testNewAccount(tc) - testLookupAccount(tc) - testForEachAccount(tc) - testForEachAccountAddress(tc) + // Reset default account + tc.account = 0 + testNewAccount(tc) + testLookupAccount(tc) + testForEachAccount(tc) + testForEachAccountAddress(tc) - // Rename account 1 "acct-create" - tc.account = 1 - testRenameAccount(tc) + // Rename account 1 "acct-create" + tc.account = 1 + testRenameAccount(tc) + } else { + // Test API for created watch-only case. + testExternalAddresses(tc) + testInternalAddresses(tc) + testMarkUsed(tc, false) + testChangePassphrase(tc) + + testNewAccount(tc) + expectedAccounts := map[string]uint32{ + defaultAccountName: DefaultAccountNum, + } + testLookupExpectedAccount(tc, expectedAccounts, 0) + //testForEachAccount(tc) + testForEachAccountAddress(tc) + } } -// testWatchingOnly tests various facets of a watching-only address +// testConvertWatchingOnly tests various facets of a watching-only address // manager such as running the full set of API tests against a newly converted // copy as well as when it is opened from an existing namespace. -func testWatchingOnly(tc *testContext) bool { +func testConvertWatchingOnly(tc *testContext) bool { + // These tests check the case where the manager was not initially + // created watch-only, but converted to watch only ... + // Make a copy of the current database so the copy can be converted to // watching only. woMgrName := "mgrtestwo.bin" @@ -1689,13 +1721,14 @@ func testWatchingOnly(tc *testContext) bool { } testManagerAPI(&testContext{ t: tc.t, + caseName: tc.caseName, db: db, rootManager: mgr, manager: scopedMgr, account: 0, create: false, watchingOnly: true, - }) + }, false) mgr.Close() // Open the watching-only manager and run all the tests again. @@ -1719,13 +1752,14 @@ func testWatchingOnly(tc *testContext) bool { testManagerAPI(&testContext{ t: tc.t, + caseName: tc.caseName, db: db, rootManager: mgr, manager: scopedMgr, account: 0, create: false, watchingOnly: true, - }) + }, false) return true } @@ -1739,7 +1773,8 @@ func testSync(tc *testContext) bool { return tc.rootManager.SetSyncedTo(ns, nil) }) if err != nil { - tc.t.Errorf("SetSyncedTo unexpected err on nil: %v", err) + tc.t.Errorf("(%s) SetSyncedTo unexpected err on nil: %v", + tc.caseName, err) return false } blockStamp := BlockStamp{ @@ -1748,8 +1783,8 @@ func testSync(tc *testContext) bool { } gotBlockStamp := tc.rootManager.SyncedTo() if gotBlockStamp != blockStamp { - tc.t.Errorf("SyncedTo unexpected block stamp on nil -- "+ - "got %v, want %v", gotBlockStamp, blockStamp) + tc.t.Errorf("(%s) SyncedTo unexpected block stamp on nil -- "+ + "got %v, want %v", tc.caseName, gotBlockStamp, blockStamp) return false } @@ -1789,39 +1824,79 @@ func testSync(tc *testContext) bool { func TestManager(t *testing.T) { t.Parallel() + tests := []struct { + name string + createdWatchingOnly bool + seed []byte + privPassphrase []byte + }{ + { + name: "created with seed", + createdWatchingOnly: false, + seed: seed, + privPassphrase: privPassphrase, + }, + { + name: "created watch-only", + createdWatchingOnly: true, + seed: 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) + } +} + +func testManagerCase(t *testing.T, caseName string, + caseCreatedWatchingOnly bool, caseSeed, casePrivPassphrase []byte) { + teardown, db := emptyDB(t) defer teardown() - // Open manager that does not exist to ensure the expected error is - // returned. - err := walletdb.View(db, func(tx walletdb.ReadTx) error { - ns := tx.ReadBucket(waddrmgrNamespaceKey) - _, err := Open(ns, pubPassphrase, &chaincfg.MainNetParams) - return err - }) - if !checkManagerError(t, "Open non-existant", err, ErrNoExist) { - return + if !caseCreatedWatchingOnly { + // Open manager that does not exist to ensure the expected error is + // returned. + err := walletdb.View(db, func(tx walletdb.ReadTx) error { + ns := tx.ReadBucket(waddrmgrNamespaceKey) + _, err := Open(ns, pubPassphrase, &chaincfg.MainNetParams) + return err + }) + if !checkManagerError(t, "Open non-existent", err, ErrNoExist) { + return + } } // Create a new manager. var mgr *Manager - err = walletdb.Update(db, func(tx walletdb.ReadWriteTx) error { + err := walletdb.Update(db, func(tx walletdb.ReadWriteTx) error { ns, err := tx.CreateTopLevelBucket(waddrmgrNamespaceKey) if err != nil { return err } err = Create( - ns, seed, pubPassphrase, privPassphrase, + ns, caseSeed, pubPassphrase, casePrivPassphrase, &chaincfg.MainNetParams, fastScrypt, time.Time{}, ) if err != nil { return err } mgr, err = Open(ns, pubPassphrase, &chaincfg.MainNetParams) + if err != nil { + return err + } + + if caseCreatedWatchingOnly { + _, err = mgr.NewScopedKeyManager( + ns, KeyScopeBIP0044, ScopeAddrMap[KeyScopeBIP0044]) + } return err }) if err != nil { - t.Errorf("Create/Open: unexpected error: %v", err) + t.Errorf("(%s) Create/Open: unexpected error: %v", caseName, err) return } @@ -1833,30 +1908,58 @@ func TestManager(t *testing.T) { err = walletdb.Update(db, func(tx walletdb.ReadWriteTx) error { ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) return Create( - ns, seed, pubPassphrase, privPassphrase, + ns, caseSeed, pubPassphrase, casePrivPassphrase, &chaincfg.MainNetParams, fastScrypt, time.Time{}, ) }) - if !checkManagerError(t, "Create existing", err, ErrAlreadyExists) { + if !checkManagerError(t, fmt.Sprintf("(%s) Create existing", caseName), + err, ErrAlreadyExists) { mgr.Close() return } - // Run all of the manager API tests in create mode and close the - // manager after they've completed scopedMgr, err := mgr.FetchScopedKeyManager(KeyScopeBIP0044) if err != nil { - t.Fatalf("unable to fetch default scope: %v", err) + t.Fatalf("(%s) unable to fetch default scope: %v", caseName, err) } + + if caseCreatedWatchingOnly { + accountKey := deriveTestAccountKey(t) + if accountKey == nil { + return + } + + acctKeyPub, err := accountKey.Neuter() + if err != nil { + t.Errorf("(%s) Neuter: unexpected error: %v", caseName, err) + return + } + + // Create the default account + err = walletdb.Update(db, func(tx walletdb.ReadWriteTx) error { + ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) + _, err = scopedMgr.NewAccountWatchingOnly( + ns, defaultAccountName, acctKeyPub) + return err + }) + if err != nil { + t.Errorf("NewAccountWatchingOnly: unexpected error: %v", err) + return + } + } + + // Run all of the manager API tests in create mode and close the + // manager after they've completed testManagerAPI(&testContext{ t: t, + caseName: caseName, db: db, manager: scopedMgr, rootManager: mgr, account: 0, create: true, - watchingOnly: false, - }) + watchingOnly: caseCreatedWatchingOnly, + }, caseCreatedWatchingOnly) mgr.Close() // Open the manager and run all the tests again in open mode which @@ -1868,44 +1971,68 @@ func TestManager(t *testing.T) { return err }) if err != nil { - t.Errorf("Open: unexpected error: %v", err) + t.Errorf("(%s) Open: unexpected error: %v", caseName, err) return } defer mgr.Close() scopedMgr, err = mgr.FetchScopedKeyManager(KeyScopeBIP0044) if err != nil { - t.Fatalf("unable to fetch default scope: %v", err) + t.Fatalf("(%s) unable to fetch default scope: %v", caseName, err) } tc := &testContext{ t: t, + caseName: caseName, db: db, manager: scopedMgr, rootManager: mgr, account: 0, create: false, - watchingOnly: false, + watchingOnly: caseCreatedWatchingOnly, } - testManagerAPI(tc) + testManagerAPI(tc, caseCreatedWatchingOnly) - // Now that the address manager has been tested in both the newly - // created and opened modes, test a watching-only version. - testWatchingOnly(tc) + if !caseCreatedWatchingOnly { + // Now that the address manager has been tested in both the newly + // created and opened modes, test a watching-only version. + testConvertWatchingOnly(tc) + } // Ensure that the manager sync state functionality works as expected. testSync(tc) - // Unlock the manager so it can be closed with it unlocked to ensure - // it works without issue. - err = walletdb.View(db, func(tx walletdb.ReadTx) error { - ns := tx.ReadBucket(waddrmgrNamespaceKey) - return mgr.Unlock(ns, privPassphrase) - }) - if err != nil { - t.Errorf("Unlock: unexpected error: %v", err) + if !caseCreatedWatchingOnly { + // Unlock the manager so it can be closed with it unlocked to ensure + // it works without issue. + err = walletdb.View(db, func(tx walletdb.ReadTx) error { + ns := tx.ReadBucket(waddrmgrNamespaceKey) + return mgr.Unlock(ns, casePrivPassphrase) + }) + if err != nil { + t.Errorf("Unlock: unexpected error: %v", err) + } } } +func deriveTestAccountKey(t *testing.T) *hdkeychain.ExtendedKey { + masterKey, err := hdkeychain.NewMaster(seed, &chaincfg.MainNetParams) + if err != nil { + t.Errorf("NewMaster: unexpected error: %v", err) + return nil + } + scopeKey, err := deriveCoinTypeKey(masterKey, KeyScopeBIP0044) + if err != nil { + t.Errorf("derive: unexpected error: %v", err) + return nil + } + accountKey, err := deriveAccountKey(scopeKey, 0) + if err != nil { + t.Errorf("derive: unexpected error: %v", err) + return nil + } + return accountKey +} + // TestManagerIncorrectVersion ensures that that the manager cannot be accessed // if its version does not match the latest version. func TestManagerHigherVersion(t *testing.T) { @@ -2454,10 +2581,145 @@ func TestNewRawAccount(t *testing.T) { t.Fatalf("unable to create new account: %v", err) } + testNewRawAccount(t, mgr, db, accountNum, scopedMgr) +} + +// TestNewRawAccountWatchingOnly tests that callers are able to +// properly create, and use watching-only raw accounts created with +// only an account number, and not a string which is eventually mapped +// to an account number. +func TestNewRawAccountWatchingOnly(t *testing.T) { + t.Parallel() + + teardown, db := emptyDB(t) + defer teardown() + + // We'll start the test by creating a new root manager that will be + // used for the duration of the test. + var mgr *Manager + err := walletdb.Update(db, func(tx walletdb.ReadWriteTx) error { + ns, err := tx.CreateTopLevelBucket(waddrmgrNamespaceKey) + if err != nil { + return err + } + err = Create( + ns, nil, pubPassphrase, nil, + &chaincfg.MainNetParams, fastScrypt, time.Time{}, + ) + if err != nil { + return err + } + mgr, err = Open(ns, pubPassphrase, &chaincfg.MainNetParams) + if err != nil { + return err + } + + _, err = mgr.NewScopedKeyManager( + ns, KeyScopeBIP0044, ScopeAddrMap[KeyScopeBIP0044]) + return err + }) + if err != nil { + t.Fatalf("create/open: unexpected error: %v", err) + } + defer mgr.Close() + + // Now that we have the manager created, we'll fetch one of the default + // scopes for usage within this test. + scopedMgr, err := mgr.FetchScopedKeyManager(KeyScopeBIP0044) + if err != nil { + t.Fatalf("unable to fetch scope %v: %v", KeyScopeBIP0044, err) + } + + accountKey := deriveTestAccountKey(t) + if accountKey == nil { + return + } + + // With the scoped manager retrieved, we'll attempt to create a new raw + // account by number. + const accountNum = 1000 + err = walletdb.Update(db, func(tx walletdb.ReadWriteTx) error { + ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) + return scopedMgr.NewRawAccountWatchingOnly(ns, accountNum, accountKey) + }) + if err != nil { + t.Fatalf("unable to create new account: %v", err) + } + + testNewRawAccount(t, mgr, db, accountNum, scopedMgr) +} + +// TestNewRawAccountHybrid is similar to TestNewRawAccountWatchingOnly +// except that the manager is created normally with a seed. This test +// shows that watch-only accounts can be added to managers with +// non-watch-only accounts. +func TestNewRawAccountHybrid(t *testing.T) { + t.Parallel() + + teardown, db := emptyDB(t) + defer teardown() + + // We'll start the test by creating a new root manager that will be + // used for the duration of the test. + var mgr *Manager + err := walletdb.Update(db, func(tx walletdb.ReadWriteTx) error { + ns, err := tx.CreateTopLevelBucket(waddrmgrNamespaceKey) + if err != nil { + return err + } + err = Create( + ns, seed, pubPassphrase, privPassphrase, + &chaincfg.MainNetParams, fastScrypt, time.Time{}, + ) + if err != nil { + return err + } + mgr, err = Open(ns, pubPassphrase, &chaincfg.MainNetParams) + return err + }) + if err != nil { + t.Fatalf("create/open: unexpected error: %v", err) + } + defer mgr.Close() + + // Now that we have the manager created, we'll fetch one of the default + // scopes for usage within this test. + scopedMgr, err := mgr.FetchScopedKeyManager(KeyScopeBIP0044) + if err != nil { + t.Fatalf("unable to fetch scope %v: %v", KeyScopeBIP0044, err) + } + + accountKey := deriveTestAccountKey(t) + if accountKey == nil { + return + } + + acctKeyPub, err := accountKey.Neuter() + if err != nil { + t.Errorf("Neuter: unexpected error: %v", err) + return + } + + // With the scoped manager retrieved, we'll attempt to create a new raw + // account by number. + const accountNum = 1000 + err = walletdb.Update(db, func(tx walletdb.ReadWriteTx) error { + ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) + return scopedMgr.NewRawAccountWatchingOnly(ns, accountNum, acctKeyPub) + }) + if err != nil { + t.Fatalf("unable to create new account: %v", err) + } + + testNewRawAccount(t, mgr, db, accountNum, scopedMgr) +} + +func testNewRawAccount(t *testing.T, mgr *Manager, db walletdb.DB, + accountNum uint32, scopedMgr *ScopedKeyManager) { // With the account created, we should be able to derive new addresses // from the account. var accountAddrNext ManagedAddress - err = walletdb.Update(db, func(tx walletdb.ReadWriteTx) error { + err := walletdb.Update(db, func(tx walletdb.ReadWriteTx) error { ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) addrs, err := scopedMgr.NextExternalAddresses( diff --git a/waddrmgr/scoped_manager.go b/waddrmgr/scoped_manager.go index 16bd83a..44f9b95 100644 --- a/waddrmgr/scoped_manager.go +++ b/waddrmgr/scoped_manager.go @@ -1187,7 +1187,7 @@ func (s *ScopedKeyManager) LastInternalAddress(ns walletdb.ReadBucket, } // NewRawAccount creates a new account for the scoped manager. This method -// differs from the NewAccount method in that this method takes the acount +// differs from the NewAccount method in that this method takes the account // number *directly*, rather than taking a string name for the account, then // mapping that to the next highest account number. func (s *ScopedKeyManager) NewRawAccount(ns walletdb.ReadWriteBucket, number uint32) error { @@ -1209,6 +1209,24 @@ func (s *ScopedKeyManager) NewRawAccount(ns walletdb.ReadWriteBucket, number uin return s.newAccount(ns, number, name) } +// NewRawAccountWatchingOnly creates a new watching only account for +// the scoped manager. This method differs from the +// NewAccountWatchingOnly method in that this method takes the account +// number *directly*, rather than taking a string name for the +// account, then mapping that to the next highest account number. +func (s *ScopedKeyManager) NewRawAccountWatchingOnly( + ns walletdb.ReadWriteBucket, number uint32, + pubKey *hdkeychain.ExtendedKey) error { + s.mtx.Lock() + defer s.mtx.Unlock() + + // As this is an ad hoc account that may not follow our normal linear + // derivation, we'll create a new name for this account based off of + // the account number. + name := fmt.Sprintf("act:%v", number) + return s.newAccountWatchingOnly(ns, number, name, pubKey) +} + // NewAccount creates and returns a new account stored in the manager based on // the given account name. If an account with the same name already exists, // ErrDuplicateAccount will be returned. Since creating a new account requires @@ -1326,6 +1344,70 @@ func (s *ScopedKeyManager) newAccount(ns walletdb.ReadWriteBucket, return putLastAccount(ns, &s.scope, account) } +// NewAccountWatchingOnly is similar to NewAccount, but for watch-only wallets. +func (s *ScopedKeyManager) NewAccountWatchingOnly(ns walletdb.ReadWriteBucket, name string, + pubKey *hdkeychain.ExtendedKey) (uint32, error) { + s.mtx.Lock() + defer s.mtx.Unlock() + + // Fetch latest account, and create a new account in the same + // transaction Fetch the latest account number to generate the next + // account number + account, err := fetchLastAccount(ns, &s.scope) + if err != nil { + return 0, err + } + account++ + + // With the name validated, we'll create a new account for the new + // contiguous account. + if err := s.newAccountWatchingOnly(ns, account, name, pubKey); err != nil { + return 0, err + } + + return account, nil +} + +// newAccountWatchingOnly is similar to newAccount, but for watching-only wallets. +// +// NOTE: This function MUST be called with the manager lock held for writes. +func (s *ScopedKeyManager) newAccountWatchingOnly(ns walletdb.ReadWriteBucket, account uint32, name string, + pubKey *hdkeychain.ExtendedKey) error { + + // Validate the account name. + if err := ValidateAccountName(name); err != nil { + return err + } + + // Check that account with the same name does not exist + _, err := s.lookupAccount(ns, name) + if err == nil { + str := fmt.Sprintf("account with the same name already exists") + return managerError(ErrDuplicateAccount, str, err) + } + + // Encrypt the default account keys with the associated crypto keys. + acctPubEnc, err := s.rootManager.cryptoKeyPub.Encrypt( + []byte(pubKey.String()), + ) + if err != nil { + str := "failed to encrypt public key for account" + return managerError(ErrCrypto, str, err) + } + + // We have the encrypted account extended keys, so save them to the + // database + err = putAccountInfo( + ns, &s.scope, account, acctPubEnc, nil, 0, 0, name, + ) + if err != nil { + return err + } + + // Save last account metadata + return putLastAccount(ns, &s.scope, account) +} + // RenameAccount renames an account stored in the manager based on the given // account number with the given name. If an account with the same name // already exists, ErrDuplicateAccount will be returned. @@ -1700,6 +1782,7 @@ func (s *ScopedKeyManager) ForEachAccount(ns walletdb.ReadBucket, } // LastAccount returns the last account stored in the manager. +// If no accounts, returns twos-complement representation of -1 func (s *ScopedKeyManager) LastAccount(ns walletdb.ReadBucket) (uint32, error) { return fetchLastAccount(ns, &s.scope) } diff --git a/wallet/loader.go b/wallet/loader.go index 5ae807e..17f761f 100644 --- a/wallet/loader.go +++ b/wallet/loader.go @@ -100,6 +100,25 @@ func (l *Loader) RunAfterLoad(fn func(*Wallet)) { func (l *Loader) CreateNewWallet(pubPassphrase, privPassphrase, seed []byte, bday time.Time) (*Wallet, error) { + return l.createNewWallet( + pubPassphrase, privPassphrase, seed, bday, false, + ) +} + +// CreateNewWatchingOnlyWallet creates a new wallet using the provided +// public passphrase. No seed or private passphrase may be provided +// since the wallet is watching-only. +func (l *Loader) CreateNewWatchingOnlyWallet(pubPassphrase []byte, + bday time.Time) (*Wallet, error) { + + return l.createNewWallet( + pubPassphrase, nil, nil, bday, true, + ) +} + +func (l *Loader) createNewWallet(pubPassphrase, privPassphrase, + seed []byte, bday time.Time, isWatchingOnly bool) (*Wallet, error) { + defer l.mu.Unlock() l.mu.Lock() @@ -127,11 +146,18 @@ func (l *Loader) CreateNewWallet(pubPassphrase, privPassphrase, seed []byte, } // Initialize the newly created database for the wallet before opening. - err = Create( - db, pubPassphrase, privPassphrase, seed, l.chainParams, bday, - ) - if err != nil { - return nil, err + if isWatchingOnly { + err = CreateWatchingOnly(db, pubPassphrase, l.chainParams, bday) + if err != nil { + return nil, err + } + } else { + err = Create( + db, pubPassphrase, privPassphrase, seed, l.chainParams, bday, + ) + if err != nil { + return nil, err + } } // Open the newly-created wallet. diff --git a/wallet/wallet.go b/wallet/wallet.go index aaa9ff6..476eea8 100644 --- a/wallet/wallet.go +++ b/wallet/wallet.go @@ -3579,23 +3579,45 @@ func (w *Wallet) Database() walletdb.DB { // 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 // 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, seed []byte, + params *chaincfg.Params, birthday time.Time) error { - // 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 + return create( + db, pubPass, privPass, seed, params, birthday, false, + ) +} + +// CreateWatchingOnly creates an new watch-only wallet, writing it to +// an empty database. No seed 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, + params *chaincfg.Params, birthday time.Time) error { + + return create( + db, pubPass, nil, nil, params, birthday, true, + ) +} + +func create(db walletdb.DB, pubPass, privPass, seed []byte, + params *chaincfg.Params, birthday time.Time, isWatchingOnly bool) 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 len(seed) < hdkeychain.MinSeedBytes || + len(seed) > hdkeychain.MaxSeedBytes { + return hdkeychain.ErrInvalidSeedLen } - seed = hdSeed - } - if len(seed) < hdkeychain.MinSeedBytes || - len(seed) > hdkeychain.MaxSeedBytes { - return hdkeychain.ErrInvalidSeedLen } return walletdb.Update(db, func(tx walletdb.ReadWriteTx) error { @@ -3609,8 +3631,7 @@ func Create(db walletdb.DB, pubPass, privPass, seed []byte, params *chaincfg.Par } err = waddrmgr.Create( - addrmgrNs, seed, pubPass, privPass, params, nil, - birthday, + addrmgrNs, seed, pubPass, privPass, params, nil, birthday, ) if err != nil { return err diff --git a/wallet/watchingonly_test.go b/wallet/watchingonly_test.go new file mode 100644 index 0000000..f7846b1 --- /dev/null +++ b/wallet/watchingonly_test.go @@ -0,0 +1,34 @@ +// Copyright (c) 2018 The btcsuite developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package wallet + +import ( + "io/ioutil" + "os" + "testing" + "time" + + "github.com/btcsuite/btcd/chaincfg" + _ "github.com/btcsuite/btcwallet/walletdb/bdb" +) + +// TestCreateWatchingOnly checks that we can construct a watching-only +// wallet. +func TestCreateWatchingOnly(t *testing.T) { + // Set up a wallet. + dir, err := ioutil.TempDir("", "watchingonly_test") + if err != nil { + t.Fatalf("Failed to create db dir: %v", err) + } + defer os.RemoveAll(dir) + + pubPass := []byte("hello") + + loader := NewLoader(&chaincfg.TestNet3Params, dir, true, 250) + _, err = loader.CreateNewWatchingOnlyWallet(pubPass, time.Now()) + if err != nil { + t.Fatalf("unable to create wallet: %v", err) + } +}