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:
parent
ada7ca077e
commit
4c5bc1b15d
8 changed files with 720 additions and 244 deletions
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -438,53 +438,61 @@ 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.locked {
|
if !m.watchingOnly {
|
||||||
return nil, managerError(ErrLocked, errLocked, nil)
|
// 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
|
// 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,
|
//
|
||||||
// therefore this can only be done if the wallet's root key hasn't been
|
// Note that the path to the coin type is requires hardened
|
||||||
// neutered.
|
// derivation, therefore this can only be done if the wallet's
|
||||||
masterRootPrivEnc, _, err := fetchMasterHDKeys(ns)
|
// root key hasn't been neutered.
|
||||||
if err != nil {
|
masterRootPrivEnc, _, err := fetchMasterHDKeys(ns)
|
||||||
return nil, err
|
if err != nil {
|
||||||
}
|
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 :=
|
||||||
if err != nil {
|
m.cryptoKeyPriv.Decrypt(masterRootPrivEnc)
|
||||||
str := fmt.Sprintf("failed to decrypt master root serialized private key")
|
if err != nil {
|
||||||
return nil, managerError(ErrLocked, str, err)
|
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
|
// 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 " +
|
||||||
return nil, managerError(ErrKeyChain, str, err)
|
"private key")
|
||||||
|
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
|
||||||
|
@ -506,19 +514,21 @@ 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
|
||||||
err = createManagerKeyScope(
|
// it along with the first account using our crypto keys.
|
||||||
ns, scope, rootPriv, m.cryptoKeyPub, m.cryptoKeyPriv,
|
err = createManagerKeyScope(
|
||||||
)
|
ns, scope, rootPriv, m.cryptoKeyPub, m.cryptoKeyPriv,
|
||||||
if err != nil {
|
)
|
||||||
return nil, err
|
if err != nil {
|
||||||
|
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
|
||||||
|
@ -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,18 +1762,6 @@ 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)
|
||||||
}
|
}
|
||||||
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.
|
// Encrypt the crypto keys with the associated master keys.
|
||||||
cryptoKeyPubEnc, err := masterKeyPub.Encrypt(cryptoKeyPub.Bytes())
|
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"
|
str := "failed to encrypt crypto public key"
|
||||||
return managerError(ErrCrypto, str, err)
|
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
|
// Use the genesis block for the passed chain as the created at block
|
||||||
// for the default.
|
// for the default.
|
||||||
|
@ -1788,52 +1777,108 @@ func Create(ns walletdb.ReadWriteBucket, seed, pubPassphrase, privPassphrase []b
|
||||||
// Create the initial sync state.
|
// Create the initial sync state.
|
||||||
syncInfo := newSyncState(createdAt, createdAt)
|
syncInfo := newSyncState(createdAt, createdAt)
|
||||||
|
|
||||||
// Save the master key params to the database.
|
|
||||||
pubParams := masterKeyPub.Marshal()
|
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
|
var privParams []byte = nil
|
||||||
// can generate the required structure with no issues.
|
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.
|
// Generate the private passphrase salt. This is used when
|
||||||
rootKey, err := hdkeychain.NewMaster(seed, chainParams)
|
// hashing passwords to detect whether an unlock can be
|
||||||
if err != nil {
|
// avoided when the manager is already unlocked.
|
||||||
str := "failed to derive master extended key"
|
var privPassphraseSalt [saltSize]byte
|
||||||
return managerError(ErrKeyChain, str, err)
|
_, err = rand.Read(privPassphraseSalt[:])
|
||||||
}
|
if err != nil {
|
||||||
rootPubKey, err := rootKey.Neuter()
|
str := "failed to read random source for passphrase salt"
|
||||||
if err != nil {
|
return managerError(ErrCrypto, str, err)
|
||||||
str := "failed to neuter master extended key"
|
}
|
||||||
return managerError(ErrKeyChain, str, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Next, for each registers default manager scope, we'll create the
|
cryptoKeyPriv, err := newCryptoKey()
|
||||||
// hardened cointype key for it, as well as the first default account.
|
if err != nil {
|
||||||
for _, defaultScope := range DefaultKeyScopes {
|
str := "failed to generate crypto private key"
|
||||||
err := createManagerKeyScope(
|
return managerError(ErrCrypto, str, err)
|
||||||
ns, defaultScope, rootKey, cryptoKeyPub, cryptoKeyPriv,
|
}
|
||||||
)
|
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 {
|
if err != nil {
|
||||||
return maybeConvertDbError(err)
|
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
|
// Save the master key params to the database.
|
||||||
// within the database in an encrypted format. This is required as in
|
err = putMasterKeyParams(ns, pubParams, privParams)
|
||||||
// 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 {
|
if err != nil {
|
||||||
return maybeConvertDbError(err)
|
return maybeConvertDbError(err)
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,31 +1620,51 @@ 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) {
|
||||||
testLocking(tc)
|
if !caseCreatedWatchingOnly {
|
||||||
testExternalAddresses(tc)
|
// Test API for normal create (w/ seed) case.
|
||||||
testInternalAddresses(tc)
|
testLocking(tc)
|
||||||
testImportPrivateKey(tc)
|
testExternalAddresses(tc)
|
||||||
testImportScript(tc)
|
testInternalAddresses(tc)
|
||||||
testMarkUsed(tc)
|
testImportPrivateKey(tc)
|
||||||
testChangePassphrase(tc)
|
testImportScript(tc)
|
||||||
|
testMarkUsed(tc, true)
|
||||||
|
testChangePassphrase(tc)
|
||||||
|
|
||||||
// Reset default account
|
// Reset default account
|
||||||
tc.account = 0
|
tc.account = 0
|
||||||
testNewAccount(tc)
|
testNewAccount(tc)
|
||||||
testLookupAccount(tc)
|
testLookupAccount(tc)
|
||||||
testForEachAccount(tc)
|
testForEachAccount(tc)
|
||||||
testForEachAccountAddress(tc)
|
testForEachAccountAddress(tc)
|
||||||
|
|
||||||
// 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,39 +1824,79 @@ 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()
|
||||||
|
|
||||||
// Open manager that does not exist to ensure the expected error is
|
if !caseCreatedWatchingOnly {
|
||||||
// returned.
|
// Open manager that does not exist to ensure the expected error is
|
||||||
err := walletdb.View(db, func(tx walletdb.ReadTx) error {
|
// returned.
|
||||||
ns := tx.ReadBucket(waddrmgrNamespaceKey)
|
err := walletdb.View(db, func(tx walletdb.ReadTx) error {
|
||||||
_, err := Open(ns, pubPassphrase, &chaincfg.MainNetParams)
|
ns := tx.ReadBucket(waddrmgrNamespaceKey)
|
||||||
return err
|
_, err := Open(ns, pubPassphrase, &chaincfg.MainNetParams)
|
||||||
})
|
return err
|
||||||
if !checkManagerError(t, "Open non-existant", err, ErrNoExist) {
|
})
|
||||||
return
|
if !checkManagerError(t, "Open non-existent", err, ErrNoExist) {
|
||||||
|
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,44 +1971,68 @@ 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)
|
||||||
|
|
||||||
// Now that the address manager has been tested in both the newly
|
if !caseCreatedWatchingOnly {
|
||||||
// created and opened modes, test a watching-only version.
|
// Now that the address manager has been tested in both the newly
|
||||||
testWatchingOnly(tc)
|
// created and opened modes, test a watching-only version.
|
||||||
|
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)
|
||||||
|
|
||||||
// Unlock the manager so it can be closed with it unlocked to ensure
|
if !caseCreatedWatchingOnly {
|
||||||
// it works without issue.
|
// Unlock the manager so it can be closed with it unlocked to ensure
|
||||||
err = walletdb.View(db, func(tx walletdb.ReadTx) error {
|
// it works without issue.
|
||||||
ns := tx.ReadBucket(waddrmgrNamespaceKey)
|
err = walletdb.View(db, func(tx walletdb.ReadTx) error {
|
||||||
return mgr.Unlock(ns, privPassphrase)
|
ns := tx.ReadBucket(waddrmgrNamespaceKey)
|
||||||
})
|
return mgr.Unlock(ns, casePrivPassphrase)
|
||||||
if err != nil {
|
})
|
||||||
t.Errorf("Unlock: unexpected error: %v", err)
|
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
|
// 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.
|
||||||
func TestManagerHigherVersion(t *testing.T) {
|
func TestManagerHigherVersion(t *testing.T) {
|
||||||
|
@ -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(
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,11 +146,18 @@ 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.
|
||||||
err = Create(
|
if isWatchingOnly {
|
||||||
db, pubPassphrase, privPassphrase, seed, l.chainParams, bday,
|
err = CreateWatchingOnly(db, pubPassphrase, l.chainParams, bday)
|
||||||
)
|
if err != nil {
|
||||||
if err != nil {
|
return nil, err
|
||||||
return nil, err
|
}
|
||||||
|
} else {
|
||||||
|
err = Create(
|
||||||
|
db, pubPassphrase, privPassphrase, seed, l.chainParams, bday,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open the newly-created wallet.
|
// Open the newly-created wallet.
|
||||||
|
|
|
@ -3579,23 +3579,45 @@ 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 {
|
||||||
|
|
||||||
// If a seed was provided, ensure that it is of valid length. Otherwise,
|
return create(
|
||||||
// we generate a random seed for the wallet with the recommended seed
|
db, pubPass, privPass, seed, params, birthday, false,
|
||||||
// length.
|
)
|
||||||
if seed == nil {
|
}
|
||||||
hdSeed, err := hdkeychain.GenerateSeed(
|
|
||||||
hdkeychain.RecommendedSeedLen)
|
// CreateWatchingOnly creates an new watch-only wallet, writing it to
|
||||||
if err != nil {
|
// an empty database. No seed can be provided as this wallet will be
|
||||||
return err
|
// 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 {
|
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(
|
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
|
||||||
|
|
34
wallet/watchingonly_test.go
Normal file
34
wallet/watchingonly_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue