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 <ken@bonsai.com>
This commit is contained in:
Dev Random 2020-04-24 17:44:21 -07:00 committed by GitHub
parent ada7ca077e
commit 4c5bc1b15d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 720 additions and 244 deletions

View file

@ -32,8 +32,9 @@ report. Package waddrmgr is licensed under the liberal ISC license.
- Import WIF keys - Import WIF keys
- Import pay-to-script-hash scripts for things such as multi-signature - Import pay-to-script-hash scripts for things such as multi-signature
transactions 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 key material
- Ability to convert to watching-only mode
- Programmatically detectable errors, including encapsulation of errors from - Programmatically detectable errors, including encapsulation of errors from
packages it relies on packages it relies on
- Address synchronization capabilities - Address synchronization capabilities

View file

@ -850,6 +850,7 @@ func forEachAccount(ns walletdb.ReadBucket, scope *KeyScope,
} }
// fetchLastAccount retrieves the last account from the database. // 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) { func fetchLastAccount(ns walletdb.ReadBucket, scope *KeyScope) (uint32, error) {
scopedBucket, err := fetchReadScopeBucket(ns, scope) scopedBucket, err := fetchReadScopeBucket(ns, scope)
if err != nil { if err != nil {
@ -859,6 +860,9 @@ func fetchLastAccount(ns walletdb.ReadBucket, scope *KeyScope) (uint32, error) {
metaBucket := scopedBucket.NestedReadBucket(metaBucketName) metaBucket := scopedBucket.NestedReadBucket(metaBucketName)
val := metaBucket.Get(lastAccountName) val := metaBucket.Get(lastAccountName)
if val == nil {
return (1 << 32) - 1, nil
}
if len(val) != 4 { if len(val) != 4 {
str := fmt.Sprintf("malformed metadata '%s' stored in database", str := fmt.Sprintf("malformed metadata '%s' stored in database",
lastAccountName) lastAccountName)

View file

@ -438,54 +438,62 @@ func (m *Manager) Close() {
// //
// TODO(roasbeef): addrtype of raw key means it'll look in scripts to possibly // TODO(roasbeef): addrtype of raw key means it'll look in scripts to possibly
// mark as gucci? // mark as gucci?
func (m *Manager) NewScopedKeyManager(ns walletdb.ReadWriteBucket, scope KeyScope, func (m *Manager) NewScopedKeyManager(ns walletdb.ReadWriteBucket,
addrSchema ScopeAddrSchema) (*ScopedKeyManager, error) { scope KeyScope, addrSchema ScopeAddrSchema) (*ScopedKeyManager, error) {
m.mtx.Lock() m.mtx.Lock()
defer m.mtx.Unlock() defer m.mtx.Unlock()
// If the manager is locked, then we can't create a new scoped manager. var rootPriv *hdkeychain.ExtendedKey
if !m.watchingOnly {
// If the manager is locked, then we can't create a new scoped
// manager.
if m.locked { if m.locked {
return nil, managerError(ErrLocked, errLocked, nil) return nil, managerError(ErrLocked, errLocked, nil)
} }
// Now that we know the manager is unlocked, we'll need to fetch the // Now that we know the manager is unlocked, we'll need to
// root master HD private key. This is required as we'll be attempting // fetch the root master HD private key. This is required as
// the following derivation: m/purpose'/cointype' // we'll be attempting the following derivation:
// m/purpose'/cointype'
// //
// Note that the path to the coin type is requires hardened derivation, // Note that the path to the coin type is requires hardened
// therefore this can only be done if the wallet's root key hasn't been // derivation, therefore this can only be done if the wallet's
// neutered. // root key hasn't been neutered.
masterRootPrivEnc, _, err := fetchMasterHDKeys(ns) masterRootPrivEnc, _, err := fetchMasterHDKeys(ns)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// If the master root private key isn't found within the database, but // If the master root private key isn't found within the
// we need to bail here as we can't create the cointype key without the // database, but we need to bail here as we can't create the
// master root private key. // cointype key without the master root private key.
if masterRootPrivEnc == nil { if masterRootPrivEnc == nil {
return nil, managerError(ErrWatchingOnly, "", nil) return nil, managerError(ErrWatchingOnly, "", nil)
} }
// Before we can derive any new scoped managers using this key, we'll // Before we can derive any new scoped managers using this
// need to fully decrypt it. // key, we'll need to fully decrypt it.
serializedMasterRootPriv, err := m.cryptoKeyPriv.Decrypt(masterRootPrivEnc) serializedMasterRootPriv, err :=
m.cryptoKeyPriv.Decrypt(masterRootPrivEnc)
if err != nil { if err != nil {
str := fmt.Sprintf("failed to decrypt master root serialized private key") str := fmt.Sprintf("failed to decrypt master root " +
"serialized private key")
return nil, managerError(ErrLocked, str, err) return nil, managerError(ErrLocked, str, err)
} }
// Now that we know the root priv is within the database, we'll decode // Now that we know the root priv is within the database,
// it into a usable object. // we'll decode it into a usable object.
rootPriv, err := hdkeychain.NewKeyFromString( rootPriv, err = hdkeychain.NewKeyFromString(
string(serializedMasterRootPriv), string(serializedMasterRootPriv),
) )
zero.Bytes(serializedMasterRootPriv) zero.Bytes(serializedMasterRootPriv)
if err != nil { if err != nil {
str := fmt.Sprintf("failed to create master extended private key") str := fmt.Sprintf("failed to create master extended " +
"private key")
return nil, managerError(ErrKeyChain, str, err) return nil, managerError(ErrKeyChain, str, err)
} }
}
// Now that we have the root private key, we'll fetch the scope bucket // Now that we have the root private key, we'll fetch the scope bucket
// so we can create the proper internal name spaces. // so we can create the proper internal name spaces.
@ -506,20 +514,22 @@ func (m *Manager) NewScopedKeyManager(ns walletdb.ReadWriteBucket, scope KeyScop
} }
scopeKey := scopeToBytes(&scope) scopeKey := scopeToBytes(&scope)
schemaBytes := scopeSchemaToBytes(&addrSchema) schemaBytes := scopeSchemaToBytes(&addrSchema)
err = scopeSchemas.Put(scopeKey[:], schemaBytes) err := scopeSchemas.Put(scopeKey[:], schemaBytes)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// With the database state created, we'll now derive the cointype key if !m.watchingOnly {
// using the master HD private key, then encrypt it along with the // With the database state created, we'll now derive the
// first account using our crypto keys. // cointype key using the master HD private key, then encrypt
// it along with the first account using our crypto keys.
err = createManagerKeyScope( err = createManagerKeyScope(
ns, scope, rootPriv, m.cryptoKeyPub, m.cryptoKeyPriv, ns, scope, rootPriv, m.cryptoKeyPub, m.cryptoKeyPriv,
) )
if err != nil { if err != nil {
return nil, err return nil, err
} }
}
// Finally, we'll register this new scoped manager with the root // Finally, we'll register this new scoped manager with the root
// manager. // manager.
@ -1298,7 +1308,7 @@ func newManager(chainParams *chaincfg.Params, masterKeyPub *snacl.SecretKey,
masterKeyPriv *snacl.SecretKey, cryptoKeyPub EncryptorDecryptor, masterKeyPriv *snacl.SecretKey, cryptoKeyPub EncryptorDecryptor,
cryptoKeyPrivEncrypted, cryptoKeyScriptEncrypted []byte, syncInfo *syncState, cryptoKeyPrivEncrypted, cryptoKeyScriptEncrypted []byte, syncInfo *syncState,
birthday time.Time, privPassphraseSalt [saltSize]byte, birthday time.Time, privPassphraseSalt [saltSize]byte,
scopedManagers map[KeyScope]*ScopedKeyManager) *Manager { scopedManagers map[KeyScope]*ScopedKeyManager, watchingOnly bool) *Manager {
m := &Manager{ m := &Manager{
chainParams: chainParams, chainParams: chainParams,
@ -1316,6 +1326,7 @@ func newManager(chainParams *chaincfg.Params, masterKeyPub *snacl.SecretKey,
scopedManagers: scopedManagers, scopedManagers: scopedManagers,
externalAddrSchemas: make(map[AddressType][]KeyScope), externalAddrSchemas: make(map[AddressType][]KeyScope),
internalAddrSchemas: make(map[AddressType][]KeyScope), internalAddrSchemas: make(map[AddressType][]KeyScope),
watchingOnly: watchingOnly,
} }
for _, sMgr := range m.scopedManagers { for _, sMgr := range m.scopedManagers {
@ -1540,9 +1551,8 @@ func loadManager(ns walletdb.ReadBucket, pubPassphrase []byte,
mgr := newManager( mgr := newManager(
chainParams, &masterKeyPub, &masterKeyPriv, chainParams, &masterKeyPub, &masterKeyPriv,
cryptoKeyPub, cryptoKeyPrivEnc, cryptoKeyScriptEnc, syncInfo, cryptoKeyPub, cryptoKeyPrivEnc, cryptoKeyScriptEnc, syncInfo,
birthday, privPassphraseSalt, scopedManagers, birthday, privPassphraseSalt, scopedManagers, watchingOnly,
) )
mgr.watchingOnly = watchingOnly
for _, scopedManager := range scopedManagers { for _, scopedManager := range scopedManagers {
scopedManager.rootManager = mgr 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 // Create creates a new address manager in the given namespace.
// 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.
// //
// All private and public keys and information are protected by secret keys // The seed must conform to the standards described in
// derived from the provided private and public passphrases. The public // hdkeychain.NewMaster and will be used to create the master root
// passphrase is required on subsequent opens of the address manager, and the // node from which all hierarchical deterministic addresses are
// private passphrase is required to unlock the address manager in order to // derived. This allows all chained addresses in the address manager
// gain access to any private keys and information. // to be recovered by using the same seed.
// //
// If a config structure is passed to the function, that configuration will // If the provided seed value is nil the address manager will be
// override the defaults. // 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 // All private and public keys and information are protected by secret
// address manager already exists in the specified namespace. // keys derived from the provided private and public passphrases. The
func Create(ns walletdb.ReadWriteBucket, seed, pubPassphrase, privPassphrase []byte, // 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, chainParams *chaincfg.Params, config *ScryptOptions,
birthday time.Time) error { 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 // Return an error if the manager has already been created in
// the given database namespace. // the given database namespace.
exists := managerExists(ns) exists := managerExists(ns)
@ -1705,13 +1728,17 @@ func Create(ns walletdb.ReadWriteBucket, seed, pubPassphrase, privPassphrase []b
} }
// Ensure the private passphrase is not empty. // Ensure the private passphrase is not empty.
if len(privPassphrase) == 0 { if !isWatchingOnly && len(privPassphrase) == 0 {
str := "private passphrase may not be empty" str := "private passphrase may not be empty"
return managerError(ErrEmptyPassphrase, str, nil) return managerError(ErrEmptyPassphrase, str, nil)
} }
// Perform the initial bucket creation and database namespace setup. // 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) return maybeConvertDbError(err)
} }
@ -1726,22 +1753,6 @@ func Create(ns walletdb.ReadWriteBucket, seed, pubPassphrase, privPassphrase []b
str := "failed to master public key" str := "failed to master public key"
return managerError(ErrCrypto, str, err) 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 // Generate new crypto public, private, and script keys. These keys are
// used to protect the actual public and private data such as addresses, // used to protect the actual public and private data such as addresses,
@ -1751,6 +1762,45 @@ func Create(ns walletdb.ReadWriteBucket, seed, pubPassphrase, privPassphrase []b
str := "failed to generate crypto public key" str := "failed to generate crypto public key"
return managerError(ErrCrypto, str, err) return managerError(ErrCrypto, str, err)
} }
// Encrypt the crypto keys with the associated master keys.
cryptoKeyPubEnc, err := masterKeyPub.Encrypt(cryptoKeyPub.Bytes())
if err != nil {
str := "failed to encrypt crypto public key"
return managerError(ErrCrypto, str, err)
}
// Use the genesis block for the passed chain as the created at block
// for the default.
createdAt := &BlockStamp{Hash: *chainParams.GenesisHash, Height: 0}
// Create the initial sync state.
syncInfo := newSyncState(createdAt, createdAt)
pubParams := masterKeyPub.Marshal()
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()
// 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)
}
cryptoKeyPriv, err := newCryptoKey() cryptoKeyPriv, err := newCryptoKey()
if err != nil { if err != nil {
str := "failed to generate crypto private key" str := "failed to generate crypto private key"
@ -1764,40 +1814,22 @@ func Create(ns walletdb.ReadWriteBucket, seed, pubPassphrase, privPassphrase []b
} }
defer cryptoKeyScript.Zero() defer cryptoKeyScript.Zero()
// Encrypt the crypto keys with the associated master keys. cryptoKeyPrivEnc, err =
cryptoKeyPubEnc, err := masterKeyPub.Encrypt(cryptoKeyPub.Bytes()) masterKeyPriv.Encrypt(cryptoKeyPriv.Bytes())
if err != nil {
str := "failed to encrypt crypto public key"
return managerError(ErrCrypto, str, err)
}
cryptoKeyPrivEnc, err := masterKeyPriv.Encrypt(cryptoKeyPriv.Bytes())
if err != nil { if err != nil {
str := "failed to encrypt crypto private key" str := "failed to encrypt crypto private key"
return managerError(ErrCrypto, str, err) return managerError(ErrCrypto, str, err)
} }
cryptoKeyScriptEnc, err := masterKeyPriv.Encrypt(cryptoKeyScript.Bytes()) cryptoKeyScriptEnc, err =
masterKeyPriv.Encrypt(cryptoKeyScript.Bytes())
if err != nil { if err != nil {
str := "failed to encrypt crypto script key" str := "failed to encrypt crypto script key"
return managerError(ErrCrypto, str, err) return managerError(ErrCrypto, str, err)
} }
// Use the genesis block for the passed chain as the created at block // Generate the BIP0044 HD key structure to ensure the
// for the default. // provided seed can generate the required structure with no
createdAt := &BlockStamp{Hash: *chainParams.GenesisHash, Height: 0} // issues.
// 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.
// Derive the master extended key from the seed. // Derive the master extended key from the seed.
rootKey, err := hdkeychain.NewMaster(seed, chainParams) rootKey, err := hdkeychain.NewMaster(seed, chainParams)
@ -1811,8 +1843,9 @@ func Create(ns walletdb.ReadWriteBucket, seed, pubPassphrase, privPassphrase []b
return managerError(ErrKeyChain, str, err) return managerError(ErrKeyChain, str, err)
} }
// Next, for each registers default manager scope, we'll create the // Next, for each registers default manager scope, we'll
// hardened cointype key for it, as well as the first default account. // create the hardened cointype key for it, as well as the
// first default account.
for _, defaultScope := range DefaultKeyScopes { for _, defaultScope := range DefaultKeyScopes {
err := createManagerKeyScope( err := createManagerKeyScope(
ns, defaultScope, rootKey, cryptoKeyPub, cryptoKeyPriv, ns, defaultScope, rootKey, cryptoKeyPub, cryptoKeyPriv,
@ -1822,14 +1855,17 @@ func Create(ns walletdb.ReadWriteBucket, seed, pubPassphrase, privPassphrase []b
} }
} }
// Before we proceed, we'll also store the root master private key // Before we proceed, we'll also store the root master private
// within the database in an encrypted format. This is required as in // key within the database in an encrypted format. This is
// the future, we may need to create additional scoped key managers. // required as in the future, we may need to create additional
masterHDPrivKeyEnc, err := cryptoKeyPriv.Encrypt([]byte(rootKey.String())) // scoped key managers.
masterHDPrivKeyEnc, err :=
cryptoKeyPriv.Encrypt([]byte(rootKey.String()))
if err != nil { if err != nil {
return maybeConvertDbError(err) return maybeConvertDbError(err)
} }
masterHDPubKeyEnc, err := cryptoKeyPub.Encrypt([]byte(rootPubKey.String())) masterHDPubKeyEnc, err :=
cryptoKeyPub.Encrypt([]byte(rootPubKey.String()))
if err != nil { if err != nil {
return maybeConvertDbError(err) return maybeConvertDbError(err)
} }
@ -1838,6 +1874,15 @@ func Create(ns walletdb.ReadWriteBucket, seed, pubPassphrase, privPassphrase []b
return maybeConvertDbError(err) return maybeConvertDbError(err)
} }
privParams = masterKeyPriv.Marshal()
}
// Save the master key params to the database.
err = putMasterKeyParams(ns, pubParams, privParams)
if err != nil {
return maybeConvertDbError(err)
}
// Save the encrypted crypto keys to the database. // Save the encrypted crypto keys to the database.
err = putCryptoKeys(ns, cryptoKeyPubEnc, cryptoKeyPrivEnc, err = putCryptoKeys(ns, cryptoKeyPubEnc, cryptoKeyPrivEnc,
cryptoKeyScriptEnc) cryptoKeyScriptEnc)
@ -1845,9 +1890,9 @@ func Create(ns walletdb.ReadWriteBucket, seed, pubPassphrase, privPassphrase []b
return maybeConvertDbError(err) 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. // database.
err = putWatchingOnly(ns, false) err = putWatchingOnly(ns, isWatchingOnly)
if err != nil { if err != nil {
return maybeConvertDbError(err) return maybeConvertDbError(err)
} }

View file

@ -17,6 +17,7 @@ import (
"github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcutil" "github.com/btcsuite/btcutil"
"github.com/btcsuite/btcutil/hdkeychain"
"github.com/btcsuite/btcwallet/snacl" "github.com/btcsuite/btcwallet/snacl"
"github.com/btcsuite/btcwallet/walletdb" "github.com/btcsuite/btcwallet/walletdb"
"github.com/davecgh/go-spew/spew" "github.com/davecgh/go-spew/spew"
@ -71,6 +72,7 @@ func failingSecretKeyGen(passphrase *[]byte,
// spent. // spent.
type testContext struct { type testContext struct {
t *testing.T t *testing.T
caseName string
db walletdb.DB db walletdb.DB
rootManager *Manager rootManager *Manager
manager *ScopedKeyManager manager *ScopedKeyManager
@ -112,7 +114,7 @@ func testNamePrefix(tc *testContext) string {
prefix = "Create " 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 // 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. // testMarkUsed ensures used addresses are flagged as such.
func testMarkUsed(tc *testContext) bool { func testMarkUsed(tc *testContext, doScript bool) bool {
tests := []struct { tests := []struct {
name string name string
typ addrType typ addrType
@ -1067,9 +1069,12 @@ func testMarkUsed(tc *testContext) bool {
}, },
} }
prefix := "MarkUsed" prefix := fmt.Sprintf("(%s) MarkUsed", tc.caseName)
chainParams := tc.manager.ChainParams() chainParams := tc.manager.ChainParams()
for i, test := range tests { for i, test := range tests {
if !doScript && test.typ == addrScriptHash {
continue
}
addrHash := test.in addrHash := test.in
var addr btcutil.Address var addr btcutil.Address
@ -1116,7 +1121,7 @@ func testMarkUsed(tc *testContext) bool {
return nil return nil
}) })
if err != 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 // testChangePassphrase ensures changes both the public and private passphrases
// works as intended. // works as intended.
func testChangePassphrase(tc *testContext) bool { func testChangePassphrase(tc *testContext) bool {
pfx := fmt.Sprintf("(%s) ", tc.caseName)
// Force an error when changing the passphrase due to failure to // Force an error when changing the passphrase due to failure to
// generate a new secret key by replacing the generation function one // generate a new secret key by replacing the generation function one
// that intentionally errors. // that intentionally errors.
testName := "ChangePassphrase (public) with invalid new secret key" testName := pfx + "ChangePassphrase (public) with invalid new secret key"
oldKeyGen := SetSecretKeyGen(failingSecretKeyGen) oldKeyGen := SetSecretKeyGen(failingSecretKeyGen)
err := walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) error { 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. // 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) SetSecretKeyGen(oldKeyGen)
err = walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) error { err = walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) error {
ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) ns := tx.ReadWriteBucket(waddrmgrNamespaceKey)
@ -1156,7 +1163,7 @@ func testChangePassphrase(tc *testContext) bool {
} }
// Change the public passphrase. // Change the public passphrase.
testName = "ChangePassphrase (public)" testName = pfx + "ChangePassphrase (public)"
err = walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) error { err = walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) error {
ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) ns := tx.ReadWriteBucket(waddrmgrNamespaceKey)
return tc.rootManager.ChangePassphrase( return tc.rootManager.ChangePassphrase(
@ -1192,7 +1199,7 @@ func testChangePassphrase(tc *testContext) bool {
// Attempt to change private passphrase with invalid old passphrase. // Attempt to change private passphrase with invalid old passphrase.
// The error should be ErrWrongPassphrase or ErrWatchingOnly depending // The error should be ErrWrongPassphrase or ErrWatchingOnly depending
// on the type of the address manager. // 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 { err = walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) error {
ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) ns := tx.ReadWriteBucket(waddrmgrNamespaceKey)
return tc.rootManager.ChangePassphrase( return tc.rootManager.ChangePassphrase(
@ -1216,7 +1223,7 @@ func testChangePassphrase(tc *testContext) bool {
} }
// Change the private passphrase. // Change the private passphrase.
testName = "ChangePassphrase (private)" testName = pfx + "ChangePassphrase (private)"
err = walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) error { err = walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) error {
ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) ns := tx.ReadWriteBucket(waddrmgrNamespaceKey)
return tc.rootManager.ChangePassphrase( return tc.rootManager.ChangePassphrase(
@ -1379,6 +1386,18 @@ func testLookupAccount(tc *testContext) bool {
defaultAccountName: DefaultAccountNum, defaultAccountName: DefaultAccountNum,
ImportedAddrAccountName: ImportedAddrAccount, 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 { for acctName, expectedAccount := range expectedAccounts {
var account uint32 var account uint32
err := walletdb.View(tc.db, func(tx walletdb.ReadTx) error { 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) lastAccount, err = tc.manager.LastAccount(ns)
return err return err
}) })
var expectedLastAccount uint32
expectedLastAccount = 1
if !tc.create {
// Existing wallet manager will have 3 accounts
expectedLastAccount = 2
}
if lastAccount != expectedLastAccount { if lastAccount != expectedLastAccount {
tc.t.Errorf("LookupAccount "+ tc.t.Errorf("LookupAccount "+
"account mismatch -- got %d, "+ "account mismatch -- got %d, "+
"want %d", lastAccount, expectedLastAccount) "want %d", lastAccount, expectedLastAccount)
return false return false
} }
// Test account lookup for default account adddress // Test account lookup for default account adddress
var expectedAccount uint32 var expectedAccount uint32
for i, addr := range expectedAddrs { for i, addr := range expectedAddrs {
@ -1608,13 +1620,15 @@ func testForEachAccountAddress(tc *testContext) bool {
// testManagerAPI tests the functions provided by the Manager API as well as // testManagerAPI tests the functions provided by the Manager API as well as
// the ManagedAddress, ManagedPubKeyAddress, and ManagedScriptAddress // the ManagedAddress, ManagedPubKeyAddress, and ManagedScriptAddress
// interfaces. // interfaces.
func testManagerAPI(tc *testContext) { func testManagerAPI(tc *testContext, caseCreatedWatchingOnly bool) {
if !caseCreatedWatchingOnly {
// Test API for normal create (w/ seed) case.
testLocking(tc) testLocking(tc)
testExternalAddresses(tc) testExternalAddresses(tc)
testInternalAddresses(tc) testInternalAddresses(tc)
testImportPrivateKey(tc) testImportPrivateKey(tc)
testImportScript(tc) testImportScript(tc)
testMarkUsed(tc) testMarkUsed(tc, true)
testChangePassphrase(tc) testChangePassphrase(tc)
// Reset default account // Reset default account
@ -1627,12 +1641,30 @@ func testManagerAPI(tc *testContext) {
// Rename account 1 "acct-create" // Rename account 1 "acct-create"
tc.account = 1 tc.account = 1
testRenameAccount(tc) 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 // 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. // 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 // Make a copy of the current database so the copy can be converted to
// watching only. // watching only.
woMgrName := "mgrtestwo.bin" woMgrName := "mgrtestwo.bin"
@ -1689,13 +1721,14 @@ func testWatchingOnly(tc *testContext) bool {
} }
testManagerAPI(&testContext{ testManagerAPI(&testContext{
t: tc.t, t: tc.t,
caseName: tc.caseName,
db: db, db: db,
rootManager: mgr, rootManager: mgr,
manager: scopedMgr, manager: scopedMgr,
account: 0, account: 0,
create: false, create: false,
watchingOnly: true, watchingOnly: true,
}) }, false)
mgr.Close() mgr.Close()
// Open the watching-only manager and run all the tests again. // Open the watching-only manager and run all the tests again.
@ -1719,13 +1752,14 @@ func testWatchingOnly(tc *testContext) bool {
testManagerAPI(&testContext{ testManagerAPI(&testContext{
t: tc.t, t: tc.t,
caseName: tc.caseName,
db: db, db: db,
rootManager: mgr, rootManager: mgr,
manager: scopedMgr, manager: scopedMgr,
account: 0, account: 0,
create: false, create: false,
watchingOnly: true, watchingOnly: true,
}) }, false)
return true return true
} }
@ -1739,7 +1773,8 @@ func testSync(tc *testContext) bool {
return tc.rootManager.SetSyncedTo(ns, nil) return tc.rootManager.SetSyncedTo(ns, nil)
}) })
if err != 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 return false
} }
blockStamp := BlockStamp{ blockStamp := BlockStamp{
@ -1748,8 +1783,8 @@ func testSync(tc *testContext) bool {
} }
gotBlockStamp := tc.rootManager.SyncedTo() gotBlockStamp := tc.rootManager.SyncedTo()
if gotBlockStamp != blockStamp { if gotBlockStamp != blockStamp {
tc.t.Errorf("SyncedTo unexpected block stamp on nil -- "+ tc.t.Errorf("(%s) SyncedTo unexpected block stamp on nil -- "+
"got %v, want %v", gotBlockStamp, blockStamp) "got %v, want %v", tc.caseName, gotBlockStamp, blockStamp)
return false return false
} }
@ -1789,9 +1824,40 @@ func testSync(tc *testContext) bool {
func TestManager(t *testing.T) { func TestManager(t *testing.T) {
t.Parallel() 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) teardown, db := emptyDB(t)
defer teardown() defer teardown()
if !caseCreatedWatchingOnly {
// Open manager that does not exist to ensure the expected error is // Open manager that does not exist to ensure the expected error is
// returned. // returned.
err := walletdb.View(db, func(tx walletdb.ReadTx) error { err := walletdb.View(db, func(tx walletdb.ReadTx) error {
@ -1799,29 +1865,38 @@ func TestManager(t *testing.T) {
_, err := Open(ns, pubPassphrase, &chaincfg.MainNetParams) _, err := Open(ns, pubPassphrase, &chaincfg.MainNetParams)
return err return err
}) })
if !checkManagerError(t, "Open non-existant", err, ErrNoExist) { if !checkManagerError(t, "Open non-existent", err, ErrNoExist) {
return return
} }
}
// Create a new manager. // Create a new manager.
var mgr *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) ns, err := tx.CreateTopLevelBucket(waddrmgrNamespaceKey)
if err != nil { if err != nil {
return err return err
} }
err = Create( err = Create(
ns, seed, pubPassphrase, privPassphrase, ns, caseSeed, pubPassphrase, casePrivPassphrase,
&chaincfg.MainNetParams, fastScrypt, time.Time{}, &chaincfg.MainNetParams, fastScrypt, time.Time{},
) )
if err != nil { if err != nil {
return err return err
} }
mgr, err = Open(ns, pubPassphrase, &chaincfg.MainNetParams) mgr, err = Open(ns, pubPassphrase, &chaincfg.MainNetParams)
if err != nil {
return err
}
if caseCreatedWatchingOnly {
_, err = mgr.NewScopedKeyManager(
ns, KeyScopeBIP0044, ScopeAddrMap[KeyScopeBIP0044])
}
return err return err
}) })
if err != nil { if err != nil {
t.Errorf("Create/Open: unexpected error: %v", err) t.Errorf("(%s) Create/Open: unexpected error: %v", caseName, err)
return return
} }
@ -1833,30 +1908,58 @@ func TestManager(t *testing.T) {
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, seed, pubPassphrase, privPassphrase, ns, caseSeed, pubPassphrase, casePrivPassphrase,
&chaincfg.MainNetParams, fastScrypt, time.Time{}, &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() mgr.Close()
return return
} }
// Run all of the manager API tests in create mode and close the
// manager after they've completed
scopedMgr, err := mgr.FetchScopedKeyManager(KeyScopeBIP0044) scopedMgr, err := mgr.FetchScopedKeyManager(KeyScopeBIP0044)
if err != nil { 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{ testManagerAPI(&testContext{
t: t, t: t,
caseName: caseName,
db: db, db: db,
manager: scopedMgr, manager: scopedMgr,
rootManager: mgr, rootManager: mgr,
account: 0, account: 0,
create: true, create: true,
watchingOnly: false, watchingOnly: caseCreatedWatchingOnly,
}) }, caseCreatedWatchingOnly)
mgr.Close() mgr.Close()
// Open the manager and run all the tests again in open mode which // Open the manager and run all the tests again in open mode which
@ -1868,43 +1971,67 @@ func TestManager(t *testing.T) {
return err return err
}) })
if err != nil { if err != nil {
t.Errorf("Open: unexpected error: %v", err) t.Errorf("(%s) Open: unexpected error: %v", caseName, err)
return return
} }
defer mgr.Close() defer mgr.Close()
scopedMgr, err = mgr.FetchScopedKeyManager(KeyScopeBIP0044) scopedMgr, err = mgr.FetchScopedKeyManager(KeyScopeBIP0044)
if err != nil { 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{ tc := &testContext{
t: t, t: t,
caseName: caseName,
db: db, db: db,
manager: scopedMgr, manager: scopedMgr,
rootManager: mgr, rootManager: mgr,
account: 0, account: 0,
create: false, create: false,
watchingOnly: false, watchingOnly: caseCreatedWatchingOnly,
} }
testManagerAPI(tc) testManagerAPI(tc, caseCreatedWatchingOnly)
if !caseCreatedWatchingOnly {
// Now that the address manager has been tested in both the newly // Now that the address manager has been tested in both the newly
// created and opened modes, test a watching-only version. // created and opened modes, test a watching-only version.
testWatchingOnly(tc) testConvertWatchingOnly(tc)
}
// Ensure that the manager sync state functionality works as expected. // Ensure that the manager sync state functionality works as expected.
testSync(tc) testSync(tc)
if !caseCreatedWatchingOnly {
// Unlock the manager so it can be closed with it unlocked to ensure // Unlock the manager so it can be closed with it unlocked to ensure
// it works without issue. // it works without issue.
err = walletdb.View(db, func(tx walletdb.ReadTx) error { err = walletdb.View(db, func(tx walletdb.ReadTx) error {
ns := tx.ReadBucket(waddrmgrNamespaceKey) ns := tx.ReadBucket(waddrmgrNamespaceKey)
return mgr.Unlock(ns, privPassphrase) return mgr.Unlock(ns, casePrivPassphrase)
}) })
if err != nil { if err != nil {
t.Errorf("Unlock: unexpected error: %v", err) 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 // TestManagerIncorrectVersion ensures that that the manager cannot be accessed
// if its version does not match the latest version. // if its version does not match the latest version.
@ -2454,10 +2581,145 @@ func TestNewRawAccount(t *testing.T) {
t.Fatalf("unable to create new account: %v", err) 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 // With the account created, we should be able to derive new addresses
// from the account. // from the account.
var accountAddrNext ManagedAddress 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) ns := tx.ReadWriteBucket(waddrmgrNamespaceKey)
addrs, err := scopedMgr.NextExternalAddresses( addrs, err := scopedMgr.NextExternalAddresses(

View file

@ -1187,7 +1187,7 @@ func (s *ScopedKeyManager) LastInternalAddress(ns walletdb.ReadBucket,
} }
// NewRawAccount creates a new account for the scoped manager. This method // 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 // number *directly*, rather than taking a string name for the account, then
// mapping that to the next highest account number. // mapping that to the next highest account number.
func (s *ScopedKeyManager) NewRawAccount(ns walletdb.ReadWriteBucket, number uint32) error { 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) 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 // 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, // the given account name. If an account with the same name already exists,
// ErrDuplicateAccount will be returned. Since creating a new account requires // 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) 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 // 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 // account number with the given name. If an account with the same name
// already exists, ErrDuplicateAccount will be returned. // 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. // 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) { func (s *ScopedKeyManager) LastAccount(ns walletdb.ReadBucket) (uint32, error) {
return fetchLastAccount(ns, &s.scope) return fetchLastAccount(ns, &s.scope)
} }

View file

@ -100,6 +100,25 @@ func (l *Loader) RunAfterLoad(fn func(*Wallet)) {
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) {
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() defer l.mu.Unlock()
l.mu.Lock() l.mu.Lock()
@ -127,12 +146,19 @@ func (l *Loader) CreateNewWallet(pubPassphrase, privPassphrase, seed []byte,
} }
// Initialize the newly created database for the wallet before opening. // Initialize the newly created database for the wallet before opening.
if isWatchingOnly {
err = CreateWatchingOnly(db, pubPassphrase, l.chainParams, bday)
if err != nil {
return nil, err
}
} else {
err = Create( err = Create(
db, pubPassphrase, privPassphrase, seed, l.chainParams, bday, db, pubPassphrase, privPassphrase, seed, l.chainParams, bday,
) )
if err != nil { if err != nil {
return nil, err return nil, err
} }
}
// Open the newly-created wallet. // Open the newly-created wallet.
w, err := Open(db, pubPassphrase, nil, l.chainParams, l.recoveryWindow) w, err := Open(db, pubPassphrase, nil, l.chainParams, l.recoveryWindow)

View file

@ -3579,9 +3579,30 @@ func (w *Wallet) Database() walletdb.DB {
// 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 // seed 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, params *chaincfg.Params, func Create(db walletdb.DB, pubPass, privPass, seed []byte,
birthday time.Time) error { params *chaincfg.Params, birthday time.Time) error {
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, // 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 // we generate a random seed for the wallet with the recommended seed
// length. // length.
@ -3597,6 +3618,7 @@ func Create(db walletdb.DB, pubPass, privPass, seed []byte, params *chaincfg.Par
len(seed) > hdkeychain.MaxSeedBytes { len(seed) > hdkeychain.MaxSeedBytes {
return hdkeychain.ErrInvalidSeedLen return hdkeychain.ErrInvalidSeedLen
} }
}
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)
@ -3609,8 +3631,7 @@ func Create(db walletdb.DB, pubPass, privPass, seed []byte, params *chaincfg.Par
} }
err = waddrmgr.Create( err = waddrmgr.Create(
addrmgrNs, seed, pubPass, privPass, params, nil, addrmgrNs, seed, pubPass, privPass, params, nil, birthday,
birthday,
) )
if err != nil { if err != nil {
return err return err

View file

@ -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)
}
}