consolidate: remove watch-only address/account support

This commit is contained in:
Roy Lee 2022-09-19 23:54:03 -07:00
parent 2b0d245b1f
commit de408d4133
19 changed files with 315 additions and 1688 deletions

View file

@ -422,12 +422,6 @@ var helpDescsEnUS = map[string]string{
"The wallet must be unlocked for this request to succeed.",
"createnewaccount-account": "Name of the new account",
// ExportWatchingWalletCmd help.
"exportwatchingwallet--synopsis": "Creates and returns a duplicate of the wallet database without any private keys to be used as a watching-only wallet.",
"exportwatchingwallet-account": "Unused (must be unset or \"*\")",
"exportwatchingwallet-download": "Unused",
"exportwatchingwallet--result0": "The watching-only database encoded as a base64 string",
// GetBestBlockCmd help.
"getbestblock--synopsis": "Returns the hash and height of the newest block in the best chain that wallet has finished syncing with.",

View file

@ -63,7 +63,6 @@ var Methods = []struct {
{"walletpassphrase", nil},
{"walletpassphrasechange", nil},
{"createnewaccount", nil},
{"exportwatchingwallet", returnsString},
{"getbestblock", []interface{}{(*btcjson.GetBestBlockResult)(nil)}},
{"getunconfirmedbalance", returnsNumber},
{"listaddresstransactions", returnsLTRArray},

View file

@ -506,9 +506,6 @@ func getAddressInfo(icmd interface{}, w *wallet.Wallet) (interface{}, error) {
// just break out now if there is an error.
script, err := ma.Script()
if err != nil {
if waddrmgr.IsError(err, waddrmgr.ErrWatchingOnly) {
result.IsWatchOnly = true
}
break
}
hexScript := hex.EncodeToString(script)
@ -2054,8 +2051,7 @@ func walletIsLocked(icmd interface{}, w *wallet.Wallet) (interface{}, error) {
}
// walletLock handles a walletlock request by locking the all account
// wallets, returning an error if any wallet is not encrypted (for example,
// a watching-only wallet).
// wallets, returning an error if any wallet is not encrypted.
func walletLock(icmd interface{}, w *wallet.Wallet) (interface{}, error) {
w.Lock()
return nil, nil

View file

@ -43,7 +43,6 @@ func helpDescsEnUS() map[string]string {
"walletpassphrase": "walletpassphrase \"passphrase\" timeout\n\nUnlock the wallet.\n\nArguments:\n1. passphrase (string, required) The wallet passphrase\n2. timeout (numeric, required) The number of seconds to wait before the wallet automatically locks\n\nResult:\nNothing\n",
"walletpassphrasechange": "walletpassphrasechange \"oldpassphrase\" \"newpassphrase\"\n\nChange the wallet passphrase.\n\nArguments:\n1. oldpassphrase (string, required) The old wallet passphrase\n2. newpassphrase (string, required) The new wallet passphrase\n\nResult:\nNothing\n",
"createnewaccount": "createnewaccount \"account\"\n\nCreates a new account.\nThe wallet must be unlocked for this request to succeed.\n\nArguments:\n1. account (string, required) Name of the new account\n\nResult:\nNothing\n",
"exportwatchingwallet": "exportwatchingwallet (\"account\" download=false)\n\nCreates and returns a duplicate of the wallet database without any private keys to be used as a watching-only wallet.\n\nArguments:\n1. account (string, optional) Unused (must be unset or \"*\")\n2. download (boolean, optional, default=false) Unused\n\nResult:\n\"value\" (string) The watching-only database encoded as a base64 string\n",
"getbestblock": "getbestblock\n\nReturns the hash and height of the newest block in the best chain that wallet has finished syncing with.\n\nArguments:\nNone\n\nResult:\n{\n \"hash\": \"value\", (string) The hash of the block\n \"height\": n, (numeric) The blockchain height of the block\n} \n",
"getunconfirmedbalance": "getunconfirmedbalance (\"account\")\n\nCalculates the unspent output value of all unmined transaction outputs for an account.\n\nArguments:\n1. account (string, optional) The account to query the unconfirmed balance for (default=\"default\")\n\nResult:\nn.nnn (numeric) Total amount of all unmined unspent outputs of the account valued in bitcoin.\n",
"listaddresstransactions": "listaddresstransactions [\"address\",...] (\"account\")\n\nReturns a JSON array of objects containing verbose details for wallet transactions pertaining some addresses.\n\nArguments:\n1. addresses (array of string, required) Addresses to filter transaction results by\n2. account (string, optional) Unused (must be unset or \"*\")\n\nResult:\n[{\n \"abandoned\": true|false, (boolean) Unset\n \"account\": \"value\", (string) DEPRECATED -- Unset\n \"address\": \"value\", (string) Payment address for a transaction output\n \"amount\": n.nnn, (numeric) The value of the transaction output valued in bitcoin\n \"bip125-replaceable\": \"value\", (string) Unset\n \"blockhash\": \"value\", (string) The hash of the block this transaction is mined in, or the empty string if unmined\n \"blockheight\": n, (numeric) The block height containing the transaction.\n \"blockindex\": n, (numeric) Unset\n \"blocktime\": n, (numeric) The Unix time of the block header this transaction is mined in, or 0 if unmined\n \"category\": \"value\", (string) The kind of transaction: \"send\" for sent transactions, \"immature\" for immature coinbase outputs, \"generate\" for mature coinbase outputs, or \"recv\" for all other received outputs. Note: A single output may be included multiple times under different categories\n \"confirmations\": n, (numeric) The number of block confirmations of the transaction\n \"fee\": n.nnn, (numeric) The total input value minus the total output value for sent transactions\n \"generated\": true|false, (boolean) Whether the transaction output is a coinbase output\n \"involveswatchonly\": true|false, (boolean) Unset\n \"label\": \"value\", (string) A comment for the address/transaction, if any\n \"time\": n, (numeric) The earliest Unix time this transaction was known to exist\n \"timereceived\": n, (numeric) The earliest Unix time this transaction was known to exist\n \"trusted\": true|false, (boolean) Unset\n \"txid\": \"value\", (string) The hash of the transaction\n \"vout\": n, (numeric) The transaction output index\n \"walletconflicts\": [\"value\",...], (array of string) Unset\n \"comment\": \"value\", (string) Unset\n \"otheraccount\": \"value\", (string) Unset\n},...]\n",
@ -57,4 +56,4 @@ var localeHelpDescs = map[string]func() map[string]string{
"en_US": helpDescsEnUS,
}
var requestUsages = "addmultisigaddress nrequired [\"key\",...] (\"account\")\ncreatemultisig nrequired [\"key\",...]\ndumpprivkey \"address\"\ngetaccount \"address\"\ngetaccountaddress \"account\"\ngetaddressesbyaccount \"account\"\ngetaddressinfo \"address\"\ngetbalance (\"account\" minconf=1)\ngetbestblockhash\ngetblockcount\ngetinfo\ngetnewaddress (\"account\" \"addresstype\")\ngetrawchangeaddress (\"account\")\ngetreceivedbyaccount \"account\" (minconf=1)\ngetreceivedbyaddress \"address\" (minconf=1)\ngettransaction \"txid\" (includewatchonly=false)\nhelp (\"command\")\nimportprivkey \"privkey\" (\"label\" rescan=true)\nkeypoolrefill (newsize=100)\nlistaccounts (minconf=1)\nlistlockunspent\nlistreceivedbyaccount (minconf=1 includeempty=false includewatchonly=false)\nlistreceivedbyaddress (minconf=1 includeempty=false includewatchonly=false)\nlistsinceblock (\"blockhash\" targetconfirmations=1 includewatchonly=false)\nlisttransactions (\"account\" count=10 from=0 includewatchonly=false)\nlistunspent (minconf=1 maxconf=9999999 [\"address\",...])\nlockunspent unlock [{\"txid\":\"value\",\"vout\":n},...]\nsendfrom \"fromaccount\" \"toaddress\" amount (minconf=1 \"comment\" \"commentto\")\nsendmany \"fromaccount\" {\"address\":amount,...} (minconf=1 \"comment\")\nsendtoaddress \"address\" amount (\"comment\" \"commentto\")\nsettxfee amount\nsignmessage \"address\" \"message\"\nsignrawtransaction \"rawtx\" ([{\"txid\":\"value\",\"vout\":n,\"scriptpubkey\":\"value\",\"redeemscript\":\"value\"},...] [\"privkey\",...] flags=\"ALL\")\nvalidateaddress \"address\"\nverifymessage \"address\" \"signature\" \"message\"\nwalletlock\nwalletpassphrase \"passphrase\" timeout\nwalletpassphrasechange \"oldpassphrase\" \"newpassphrase\"\ncreatenewaccount \"account\"\nexportwatchingwallet (\"account\" download=false)\ngetbestblock\ngetunconfirmedbalance (\"account\")\nlistaddresstransactions [\"address\",...] (\"account\")\nlistalltransactions (\"account\")\nrenameaccount \"oldaccount\" \"newaccount\"\nwalletislocked"
var requestUsages = "addmultisigaddress nrequired [\"key\",...] (\"account\")\ncreatemultisig nrequired [\"key\",...]\ndumpprivkey \"address\"\ngetaccount \"address\"\ngetaccountaddress \"account\"\ngetaddressesbyaccount \"account\"\ngetaddressinfo \"address\"\ngetbalance (\"account\" minconf=1)\ngetbestblockhash\ngetblockcount\ngetinfo\ngetnewaddress (\"account\" \"addresstype\")\ngetrawchangeaddress (\"account\")\ngetreceivedbyaccount \"account\" (minconf=1)\ngetreceivedbyaddress \"address\" (minconf=1)\ngettransaction \"txid\" (includewatchonly=false)\nhelp (\"command\")\nimportprivkey \"privkey\" (\"label\" rescan=true)\nkeypoolrefill (newsize=100)\nlistaccounts (minconf=1)\nlistlockunspent\nlistreceivedbyaccount (minconf=1 includeempty=false includewatchonly=false)\nlistreceivedbyaddress (minconf=1 includeempty=false includewatchonly=false)\nlistsinceblock (\"blockhash\" targetconfirmations=1 includewatchonly=false)\nlisttransactions (\"account\" count=10 from=0 includewatchonly=false)\nlistunspent (minconf=1 maxconf=9999999 [\"address\",...])\nlockunspent unlock [{\"txid\":\"value\",\"vout\":n},...]\nsendfrom \"fromaccount\" \"toaddress\" amount (minconf=1 \"comment\" \"commentto\")\nsendmany \"fromaccount\" {\"address\":amount,...} (minconf=1 \"comment\")\nsendtoaddress \"address\" amount (\"comment\" \"commentto\")\nsettxfee amount\nsignmessage \"address\" \"message\"\nsignrawtransaction \"rawtx\" ([{\"txid\":\"value\",\"vout\":n,\"scriptpubkey\":\"value\",\"redeemscript\":\"value\"},...] [\"privkey\",...] flags=\"ALL\")\nvalidateaddress \"address\"\nverifymessage \"address\" \"signature\" \"message\"\nwalletlock\nwalletpassphrase \"passphrase\" timeout\nwalletpassphrasechange \"oldpassphrase\" \"newpassphrase\"\ncreatenewaccount \"account\"\ngetbestblock\ngetunconfirmedbalance (\"account\")\nlistaddresstransactions [\"address\",...] (\"account\")\nlistalltransactions (\"account\")\nrenameaccount \"oldaccount\" \"newaccount\"\nwalletislocked"

View file

@ -101,8 +101,7 @@ type ManagedPubKeyAddress interface {
ExportPubKey() string
// PrivKey returns the private key for the address. It can fail if the
// address manager is watching-only or locked, or the address does not
// have any keys.
// address manager is locked, or the address does not have any keys.
PrivKey() (*btcec.PrivateKey, error)
// ExportPrivKey returns the private key associated with the address
@ -155,12 +154,6 @@ func (a *managedAddress) unlock(key EncryptorDecryptor) ([]byte, error) {
a.privKeyMutex.Lock()
defer a.privKeyMutex.Unlock()
// If the address belongs to a watch-only account, the encrypted private
// key won't be present, so we'll return an error.
if len(a.privKeyEncrypted) == 0 {
return nil, managerError(ErrWatchingOnly, errWatchingOnly, nil)
}
if len(a.privKeyCT) == 0 {
privKey, err := key.Decrypt(a.privKeyEncrypted)
if err != nil {
@ -284,14 +277,10 @@ func (a *managedAddress) ExportPubKey() string {
}
// PrivKey returns the private key for the address. It can fail if the address
// manager is watching-only or locked, or the address does not have any keys.
// manager is locked, or the address does not have any keys.
//
// This is part of the ManagedPubKeyAddress interface implementation.
func (a *managedAddress) PrivKey() (*btcec.PrivateKey, error) {
// No private keys are available for a watching-only address manager.
if a.manager.rootManager.WatchOnly() {
return nil, managerError(ErrWatchingOnly, errWatchingOnly, nil)
}
a.manager.mtx.Lock()
defer a.manager.mtx.Unlock()
@ -623,10 +612,6 @@ func (a *scriptAddress) Used(ns walletdb.ReadBucket) bool {
//
// This is part of the ManagedAddress interface implementation.
func (a *scriptAddress) Script() ([]byte, error) {
// No script is available for a watching-only address manager.
if a.manager.rootManager.WatchOnly() {
return nil, managerError(ErrWatchingOnly, errWatchingOnly, nil)
}
a.manager.mtx.Lock()
defer a.manager.mtx.Unlock()
@ -721,10 +706,6 @@ func (a *witnessScriptAddress) Used(ns walletdb.ReadBucket) bool {
//
// This is part of the ManagedAddress interface implementation.
func (a *witnessScriptAddress) Script() ([]byte, error) {
// No script is available for a watching-only address manager.
if a.isSecretScript && a.manager.rootManager.WatchOnly() {
return nil, managerError(ErrWatchingOnly, errWatchingOnly, nil)
}
a.manager.mtx.Lock()
defer a.manager.mtx.Unlock()

View file

@ -6,7 +6,6 @@
package waddrmgr
import (
"bytes"
"crypto/sha256"
"encoding/binary"
"errors"
@ -85,12 +84,6 @@ const (
// database. This is an account that re-uses the key derivation schema
// of BIP0044-like accounts.
accountDefault accountType = 0 // not iota as they need to be stable
// accountWatchOnly is the account type used for storing watch-only
// accounts within the database. This is an account that re-uses the key
// derivation schema of BIP0044-like accounts and does not store private
// keys.
accountWatchOnly accountType = 1
)
// dbAccountRow houses information stored about an account in the database.
@ -110,18 +103,6 @@ type dbDefaultAccountRow struct {
name string
}
// dbWatchOnlyAccountRow houses additional information stored about a watch-only
// account in the databse.
type dbWatchOnlyAccountRow struct {
dbAccountRow
pubKeyEncrypted []byte
masterKeyFingerprint uint32
nextExternalIndex uint32
nextInternalIndex uint32
name string
addrSchema *ScopeAddrSchema
}
// dbAddressRow houses common information stored about an address in the
// database.
type dbAddressRow struct {
@ -298,7 +279,6 @@ var (
cryptoPrivKeyName = []byte("cpriv")
cryptoPubKeyName = []byte("cpub")
cryptoScriptKeyName = []byte("cscript")
watchingOnlyName = []byte("watchonly")
// Sync related key names (sync bucket).
syncedToName = []byte("syncedto")
@ -439,9 +419,7 @@ func putManagerVersion(ns walletdb.ReadWriteBucket, version uint32) error {
}
// fetchMasterKeyParams loads the master key parameters needed to derive them
// (when given the correct user-supplied passphrase) from the database. Either
// returned value can be nil, but in practice only the private key params will
// be nil for a watching-only database.
// (when given the correct user-supplied passphrase) from the database.
func fetchMasterKeyParams(ns walletdb.ReadBucket) ([]byte, []byte, error) {
bucket := ns.NestedReadBucket(mainBucketName)
@ -604,8 +582,7 @@ func fetchMasterHDKeys(ns walletdb.ReadBucket) ([]byte, []byte) {
// fetchCryptoKeys loads the encrypted crypto keys which are in turn used to
// protect the extended keys, imported keys, and scripts. Any of the returned
// values can be nil, but in practice only the crypto private and script keys
// will be nil for a watching-only database.
// values can be nil
func fetchCryptoKeys(ns walletdb.ReadBucket) ([]byte, []byte, []byte, error) {
bucket := ns.NestedReadBucket(mainBucketName)
@ -672,35 +649,6 @@ func putCryptoKeys(ns walletdb.ReadWriteBucket, pubKeyEncrypted, privKeyEncrypte
return nil
}
// fetchWatchingOnly loads the watching-only flag from the database.
func fetchWatchingOnly(ns walletdb.ReadBucket) (bool, error) {
bucket := ns.NestedReadBucket(mainBucketName)
buf := bucket.Get(watchingOnlyName)
if len(buf) != 1 {
str := "malformed watching-only flag stored in database"
return false, managerError(ErrDatabase, str, nil)
}
return buf[0] != 0, nil
}
// putWatchingOnly stores the watching-only flag to the database.
func putWatchingOnly(ns walletdb.ReadWriteBucket, watchingOnly bool) error {
bucket := ns.NestedReadWriteBucket(mainBucketName)
var encoded byte
if watchingOnly {
encoded = 1
}
if err := bucket.Put(watchingOnlyName, []byte{encoded}); err != nil {
str := "failed to store watching only flag"
return managerError(ErrDatabase, str, err)
}
return nil
}
// deserializeAccountRow deserializes the passed serialized account information.
// This is used as a common base for the various account types to deserialize
// the common parts.
@ -817,159 +765,6 @@ func serializeDefaultAccountRow(encryptedPubKey, encryptedPrivKey []byte,
return rawData
}
// deserializeWatchOnlyAccountRow deserializes the raw data from the passed
// account row as a watch-only account.
func deserializeWatchOnlyAccountRow(accountID []byte,
row *dbAccountRow) (*dbWatchOnlyAccountRow, error) {
// The serialized BIP0044 watch-only account raw data format is:
// <encpubkeylen><encpubkey><masterkeyfingerprint><nextextidx>
// <nextintidx><namelen><name>
//
// 4 bytes encrypted pubkey len + encrypted pubkey + 4 bytes master key
// fingerprint + 4 bytes next external index + 4 bytes next internal
// index + 4 bytes name len + name + 1 byte addr schema exists + 2 bytes
// addr schema (if exists)
// Given the above, the length of the entry must be at a minimum
// the constant value sizes.
if len(row.rawData) < 21 {
str := fmt.Sprintf("malformed serialized watch-only account "+
"for key %x", accountID)
return nil, managerError(ErrDatabase, str, nil)
}
retRow := dbWatchOnlyAccountRow{
dbAccountRow: *row,
}
r := bytes.NewReader(row.rawData)
var pubLen uint32
err := binary.Read(r, binary.LittleEndian, &pubLen)
if err != nil {
return nil, err
}
retRow.pubKeyEncrypted = make([]byte, pubLen)
err = binary.Read(r, binary.LittleEndian, &retRow.pubKeyEncrypted)
if err != nil {
return nil, err
}
err = binary.Read(r, binary.LittleEndian, &retRow.masterKeyFingerprint)
if err != nil {
return nil, err
}
err = binary.Read(r, binary.LittleEndian, &retRow.nextExternalIndex)
if err != nil {
return nil, err
}
err = binary.Read(r, binary.LittleEndian, &retRow.nextInternalIndex)
if err != nil {
return nil, err
}
var nameLen uint32
err = binary.Read(r, binary.LittleEndian, &nameLen)
if err != nil {
return nil, err
}
name := make([]byte, nameLen)
err = binary.Read(r, binary.LittleEndian, &name)
if err != nil {
return nil, err
}
retRow.name = string(name)
var addrSchemaExists bool
err = binary.Read(r, binary.LittleEndian, &addrSchemaExists)
if err != nil {
return nil, err
}
if addrSchemaExists {
var addrSchemaBytes [2]byte
err = binary.Read(r, binary.LittleEndian, &addrSchemaBytes)
if err != nil {
return nil, err
}
retRow.addrSchema = scopeSchemaFromBytes(addrSchemaBytes[:])
}
return &retRow, nil
}
// serializeWatchOnlyAccountRow returns the serialization of the raw data field
// for a watch-only account.
func serializeWatchOnlyAccountRow(encryptedPubKey []byte, masterKeyFingerprint,
nextExternalIndex, nextInternalIndex uint32, name string,
addrSchema *ScopeAddrSchema) ([]byte, error) {
// The serialized BIP0044 account raw data format is:
// <encpubkeylen><encpubkey><masterkeyfingerprint><nextextidx>
// <nextintidx><namelen><name>
//
// 4 bytes encrypted pubkey len + encrypted pubkey + 4 bytes master key
// fingerprint + 4 bytes next external index + 4 bytes next internal
// index + 4 bytes name len + name + 1 byte addr schema exists + 2 bytes
// addr schema (if exists)
pubLen := uint32(len(encryptedPubKey))
nameLen := uint32(len(name))
addrSchemaExists := addrSchema != nil
var addrSchemaBytes []byte
if addrSchemaExists {
addrSchemaBytes = scopeSchemaToBytes(addrSchema)
}
bufLen := 21 + pubLen + nameLen + uint32(len(addrSchemaBytes))
buf := bytes.NewBuffer(make([]byte, 0, bufLen))
err := binary.Write(buf, binary.LittleEndian, pubLen)
if err != nil {
return nil, err
}
err = binary.Write(buf, binary.LittleEndian, encryptedPubKey)
if err != nil {
return nil, err
}
err = binary.Write(buf, binary.LittleEndian, masterKeyFingerprint)
if err != nil {
return nil, err
}
err = binary.Write(buf, binary.LittleEndian, nextExternalIndex)
if err != nil {
return nil, err
}
err = binary.Write(buf, binary.LittleEndian, nextInternalIndex)
if err != nil {
return nil, err
}
err = binary.Write(buf, binary.LittleEndian, nameLen)
if err != nil {
return nil, err
}
err = binary.Write(buf, binary.LittleEndian, []byte(name))
if err != nil {
return nil, err
}
err = binary.Write(buf, binary.LittleEndian, addrSchemaExists)
if err != nil {
return nil, err
}
if addrSchemaExists {
err = binary.Write(buf, binary.LittleEndian, addrSchemaBytes)
if err != nil {
return nil, err
}
}
return buf.Bytes(), nil
}
// forEachKeyScope calls the given function for each known manager scope
// within the set of scopes known by the root manager.
func forEachKeyScope(ns walletdb.ReadBucket, fn func(KeyScope) error) error {
@ -1108,8 +903,6 @@ func fetchAccountInfo(ns walletdb.ReadBucket, scope *KeyScope,
switch row.acctType {
case accountDefault:
return deserializeDefaultAccountRow(accountID, row)
case accountWatchOnly:
return deserializeWatchOnlyAccountRow(accountID, row)
}
str := fmt.Sprintf("unsupported account type '%d'", row.acctType)
@ -1270,30 +1063,6 @@ func putDefaultAccountInfo(ns walletdb.ReadWriteBucket, scope *KeyScope,
return putAccountInfo(ns, scope, account, &acctRow, name)
}
// putWatchOnlyAccountInfo stores the provided watch-only account information to
// the database.
func putWatchOnlyAccountInfo(ns walletdb.ReadWriteBucket, scope *KeyScope,
account uint32, encryptedPubKey []byte, masterKeyFingerprint,
nextExternalIndex, nextInternalIndex uint32, name string,
addrSchema *ScopeAddrSchema) error {
rawData, err := serializeWatchOnlyAccountRow(
encryptedPubKey, masterKeyFingerprint, nextExternalIndex,
nextInternalIndex, name, addrSchema,
)
if err != nil {
return err
}
// TODO(roasbeef): pass scope bucket directly??
acctRow := dbAccountRow{
acctType: accountWatchOnly,
rawData: rawData,
}
return putAccountInfo(ns, scope, account, &acctRow, name)
}
// putAccountInfo stores the provided account information to the database.
func putAccountInfo(ns walletdb.ReadWriteBucket, scope *KeyScope,
account uint32, acctRow *dbAccountRow, name string) error {
@ -1763,32 +1532,6 @@ func putChainedAddress(ns walletdb.ReadWriteBucket, scope *KeyScope,
arow.pubKeyEncrypted, arow.privKeyEncrypted,
nextExternalIndex, nextInternalIndex, arow.name,
)
case accountWatchOnly:
arow, err := deserializeWatchOnlyAccountRow(accountID, row)
if err != nil {
return err
}
// Increment the appropriate next index depending on whether the
// branch is internal or external.
nextExternalIndex := arow.nextExternalIndex
nextInternalIndex := arow.nextInternalIndex
if branch == InternalBranch {
nextInternalIndex = index + 1
} else {
nextExternalIndex = index + 1
}
// Reserialize the account with the updated index and store it.
row.rawData, err = serializeWatchOnlyAccountRow(
arow.pubKeyEncrypted, arow.masterKeyFingerprint,
nextExternalIndex, nextInternalIndex, arow.name,
arow.addrSchema,
)
if err != nil {
return err
}
}
err = bucket.Put(accountID, serializeAccountRow(row))
@ -1982,12 +1725,6 @@ func forEachActiveAddress(ns walletdb.ReadBucket, scope *KeyScope,
}
// deletePrivateKeys removes all private key material from the database.
//
// NOTE: Care should be taken when calling this function. It is primarily
// intended for use in converting to a watching-only copy. Removing the private
// keys from the main database without also marking it watching-only will result
// in an unusable database. It will also make any imported scripts and private
// keys unrecoverable unless there is a backup copy available.
func deletePrivateKeys(ns walletdb.ReadWriteBucket) error {
bucket := ns.NestedReadWriteBucket(mainBucketName)
@ -2058,9 +1795,6 @@ func deletePrivateKeys(ns walletdb.ReadWriteBucket) error {
str := "failed to delete account private key"
return managerError(ErrDatabase, str, err)
}
// Watch-only accounts don't contain any private keys.
case accountWatchOnly:
}
return nil

View file

@ -29,10 +29,6 @@ var (
// errLocked is the common error description used for the ErrLocked
// error code.
errLocked = "address manager is locked"
// errWatchingOnly is the common error description used for the
// ErrWatchingOnly error code.
errWatchingOnly = "address manager is watching-only"
)
// ErrorCode identifies a kind of error.
@ -86,11 +82,6 @@ const (
// manager to be unlocked, was requested on a locked account manager.
ErrLocked
// ErrWatchingOnly indicates that an operation, which requires the
// account manager to have access to private data, was requested on
// a watching-only account manager.
ErrWatchingOnly
// ErrInvalidAccount indicates that the requested account is not valid.
ErrInvalidAccount
@ -157,7 +148,6 @@ var errorCodeStrings = map[ErrorCode]string{
ErrCoinTypeTooHigh: "ErrCoinTypeTooHigh",
ErrAccountNumTooHigh: "ErrAccountNumTooHigh",
ErrLocked: "ErrLocked",
ErrWatchingOnly: "ErrWatchingOnly",
ErrInvalidAccount: "ErrInvalidAccount",
ErrAddressNotFound: "ErrAddressNotFound",
ErrAccountNotFound: "ErrAccountNotFound",

View file

@ -28,7 +28,6 @@ func TestErrorCodeStringer(t *testing.T) {
{waddrmgr.ErrCoinTypeTooHigh, "ErrCoinTypeTooHigh"},
{waddrmgr.ErrAccountNumTooHigh, "ErrAccountNumTooHigh"},
{waddrmgr.ErrLocked, "ErrLocked"},
{waddrmgr.ErrWatchingOnly, "ErrWatchingOnly"},
{waddrmgr.ErrInvalidAccount, "ErrInvalidAccount"},
{waddrmgr.ErrAddressNotFound, "ErrAddressNotFound"},
{waddrmgr.ErrAccountNotFound, "ErrAccountNotFound"},

View file

@ -42,16 +42,6 @@ const (
// ImportedAddrAccountName is the name of the imported account.
ImportedAddrAccountName = "imported"
// ImportedWatchonlyAddrAccount is the account number to use for all
// imported watchonly addresses, such as public keys and addresses.
// This is useful since normal accounts are derived from the root
// hierarchical deterministic key and imported addresses do not fit
// into that model.
ImportedWatchonlyAddrAccount = hdkeychain.HardenedKeyStart - 2 // 2^31 - 2
// ImportedWatchonlyAddrAccountName is the name of the imported watchonly account.
ImportedWatchonlyAddrAccountName = "imported-watchonly"
// DefaultAccountNum is the number of the default account.
DefaultAccountNum = 0
@ -221,10 +211,6 @@ type AccountProperties struct {
// KeyScope is the key scope the account belongs to.
KeyScope KeyScope
// IsWatchOnly indicates whether the is set up as watch-only, i.e., it
// doesn't contain any private key information.
IsWatchOnly bool
// AddrSchema, if non-nil, specifies an address schema override for
// address generation only applicable to the account.
AddrSchema *ScopeAddrSchema
@ -352,12 +338,11 @@ type Manager struct {
externalAddrSchemas map[AddressType][]KeyScope
internalAddrSchemas map[AddressType][]KeyScope
syncState syncState
watchingOnly bool
birthday time.Time
locked bool
closed bool
chainParams *chaincfg.Params
syncState syncState
birthday time.Time
locked bool
closed bool
chainParams *chaincfg.Params
// masterKeyPub is the secret key used to secure the cryptoKeyPub key
// and masterKeyPriv is the secret key used to secure the cryptoKeyPriv
@ -397,46 +382,6 @@ type Manager struct {
hashedPrivPassphrase [sha512.Size]byte
}
// WatchOnly returns true if the root manager is in watch only mode, and false
// otherwise.
func (m *Manager) WatchOnly() bool {
m.mtx.RLock()
defer m.mtx.RUnlock()
return m.watchOnly()
}
// watchOnly returns true if the root manager is in watch only mode, and false
// otherwise.
//
// NOTE: This method requires the Manager's lock to be held.
func (m *Manager) watchOnly() bool {
return m.watchingOnly
}
// IsWatchOnlyAccount determines if the account with the given key scope is set
// up as watch-only.
func (m *Manager) IsWatchOnlyAccount(ns walletdb.ReadBucket, keyScope KeyScope,
account uint32) (bool, error) {
if m.WatchOnly() {
return true, nil
}
if account == ImportedAddrAccount {
return false, nil
}
if account == ImportedWatchonlyAddrAccount {
return true, nil
}
scopedMgr, err := m.FetchScopedKeyManager(keyScope)
if err != nil {
return false, err
}
return scopedMgr.IsWatchOnlyAccount(ns, account)
}
// lock performs a best try effort to remove and zero all secret keys associated
// with the address manager.
//
@ -497,7 +442,7 @@ func (m *Manager) Close() {
}
// Attempt to clear private key material from memory.
if !m.watchingOnly && !m.locked {
if !m.locked {
m.lock()
}
@ -525,51 +470,50 @@ func (m *Manager) NewScopedKeyManager(ns walletdb.ReadWriteBucket,
defer m.mtx.Unlock()
var rootPriv *hdkeychain.ExtendedKey
if !m.watchingOnly {
// If the manager is locked, then we can't create a new scoped
// manager.
if m.locked {
return nil, managerError(ErrLocked, errLocked, nil)
}
// If the manager is locked, then we can't create a new scoped
// manager.
if m.locked {
return nil, managerError(ErrLocked, errLocked, nil)
}
// Now that we know the manager is unlocked, we'll need to
// fetch the root master HD private key. This is required as
// we'll be attempting the following derivation:
// m/purpose'/cointype'
//
// Note that the path to the coin type is requires hardened
// derivation, therefore this can only be done if the wallet's
// root key hasn't been neutered.
masterRootPrivEnc, _ := fetchMasterHDKeys(ns)
// Now that we know the manager is unlocked, we'll need to
// fetch the root master HD private key. This is required as
// we'll be attempting the following derivation:
// m/purpose'/cointype'
//
// Note that the path to the coin type is requires hardened
// derivation, therefore this can only be done if the wallet's
// root key hasn't been neutered.
masterRootPrivEnc, _ := fetchMasterHDKeys(ns)
// If the master root private key isn't found within the
// database, but we need to bail here as we can't create the
// cointype key without the master root private key.
if masterRootPrivEnc == nil {
return nil, managerError(ErrWatchingOnly, "", nil)
}
// If the master root private key isn't found within the
// database, but we need to bail here as we can't create the
// cointype key without the master root private key.
if masterRootPrivEnc == nil {
str := fmt.Sprintf("no master root private key found")
return nil, managerError(ErrKeyChain, str, nil)
}
// Before we can derive any new scoped managers using this
// key, we'll need to fully decrypt it.
serializedMasterRootPriv, err :=
m.cryptoKeyPriv.Decrypt(masterRootPrivEnc)
if err != nil {
str := fmt.Sprintf("failed to decrypt master root " +
"serialized private key")
return nil, managerError(ErrLocked, str, err)
}
// Before we can derive any new scoped managers using this
// key, we'll need to fully decrypt it.
serializedMasterRootPriv, err :=
m.cryptoKeyPriv.Decrypt(masterRootPrivEnc)
if err != nil {
str := fmt.Sprintf("failed to decrypt master root " +
"serialized private key")
return nil, managerError(ErrLocked, str, err)
}
// Now that we know the root priv is within the database,
// we'll decode it into a usable object.
rootPriv, err = hdkeychain.NewKeyFromString(
string(serializedMasterRootPriv),
)
zero.Bytes(serializedMasterRootPriv)
if err != nil {
str := fmt.Sprintf("failed to create master extended " +
"private key")
return nil, managerError(ErrKeyChain, str, err)
}
// Now that we know the root priv is within the database,
// we'll decode it into a usable object.
rootPriv, err = hdkeychain.NewKeyFromString(
string(serializedMasterRootPriv),
)
zero.Bytes(serializedMasterRootPriv)
if err != nil {
str := fmt.Sprintf("failed to create master extended " +
"private key")
return nil, managerError(ErrKeyChain, str, err)
}
// Now that we have the root private key, we'll fetch the scope bucket
@ -591,21 +535,19 @@ func (m *Manager) NewScopedKeyManager(ns walletdb.ReadWriteBucket,
}
scopeKey := scopeToBytes(&scope)
schemaBytes := scopeSchemaToBytes(&addrSchema)
err := scopeSchemas.Put(scopeKey[:], schemaBytes)
err = scopeSchemas.Put(scopeKey[:], schemaBytes)
if err != nil {
return nil, err
}
if !m.watchingOnly {
// With the database state created, we'll now derive the
// cointype key using the master HD private key, then encrypt
// it along with the first account using our crypto keys.
err = createManagerKeyScope(
ns, scope, rootPriv, m.cryptoKeyPub, m.cryptoKeyPriv,
)
if err != nil {
return nil, err
}
// With the database state created, we'll now derive the
// cointype key using the master HD private key, then encrypt
// it along with the first account using our crypto keys.
err = createManagerKeyScope(
ns, scope, rootPriv, m.cryptoKeyPub, m.cryptoKeyPriv,
)
if err != nil {
return nil, err
}
// Finally, we'll register this new scoped manager with the root
@ -886,19 +828,13 @@ func (m *Manager) ChainParams() *chaincfg.Params {
}
// ChangePassphrase changes either the public or private passphrase to the
// provided value depending on the private flag. In order to change the
// private password, the address manager must not be watching-only. The new
// passphrase keys are derived using the scrypt parameters in the options, so
// changing the passphrase may be used to bump the computational difficulty
// needed to brute force the passphrase.
// provided value depending on the private flag. The new passphrase keys are
// derived using the scrypt parameters in the options, so changing the
// passphrase may be used to bump the computational difficulty needed to brute
// force the passphrase.
func (m *Manager) ChangePassphrase(ns walletdb.ReadWriteBucket, oldPassphrase,
newPassphrase []byte, private bool, config *ScryptOptions) error {
// No private passphrase to change for a watching-only address manager.
if private && m.watchingOnly {
return managerError(ErrWatchingOnly, errWatchingOnly, nil)
}
m.mtx.Lock()
defer m.mtx.Unlock()
@ -1045,91 +981,6 @@ func (m *Manager) ChangePassphrase(ns walletdb.ReadWriteBucket, oldPassphrase,
return nil
}
// ConvertToWatchingOnly converts the current address manager to a locked
// watching-only address manager.
//
// WARNING: This function removes private keys from the existing address manager
// which means they will no longer be available. Typically the caller will make
// a copy of the existing wallet database and modify the copy since otherwise it
// would mean permanent loss of any imported private keys and scripts.
//
// Executing this function on a manager that is already watching-only will have
// no effect.
func (m *Manager) ConvertToWatchingOnly(ns walletdb.ReadWriteBucket) error {
m.mtx.Lock()
defer m.mtx.Unlock()
// Exit now if the manager is already watching-only.
if m.watchingOnly {
return nil
}
var err error
// Remove all private key material and mark the new database as
// watching only.
if err := deletePrivateKeys(ns); err != nil {
return maybeConvertDbError(err)
}
err = putWatchingOnly(ns, true)
if err != nil {
return maybeConvertDbError(err)
}
// Lock the manager to remove all clear text private key material from
// memory if needed.
if !m.locked {
m.lock()
}
// This section clears and removes the encrypted private key material
// that is ordinarily used to unlock the manager. Since the the manager
// is being converted to watching-only, the encrypted private key
// material is no longer needed.
// Clear and remove all of the encrypted acount private keys.
for _, manager := range m.scopedManagers {
for _, acctInfo := range manager.acctInfo {
zero.Bytes(acctInfo.acctKeyEncrypted)
acctInfo.acctKeyEncrypted = nil
}
}
// Clear and remove encrypted private keys and encrypted scripts from
// all address entries.
for _, manager := range m.scopedManagers {
for _, ma := range manager.addrs {
switch addr := ma.(type) {
case *managedAddress:
zero.Bytes(addr.privKeyEncrypted)
addr.privKeyEncrypted = nil
case *scriptAddress:
zero.Bytes(addr.scriptEncrypted)
addr.scriptEncrypted = nil
}
}
}
// Clear and remove encrypted private and script crypto keys.
zero.Bytes(m.cryptoKeyScriptEncrypted)
m.cryptoKeyScriptEncrypted = nil
m.cryptoKeyScript = nil
zero.Bytes(m.cryptoKeyPrivEncrypted)
m.cryptoKeyPrivEncrypted = nil
m.cryptoKeyPriv = nil
// The master private key is derived from a passphrase when the manager
// is unlocked, so there is no encrypted version to zero. However,
// it is no longer needed, so nil it.
m.masterKeyPriv = nil
// Mark the manager watching-only.
m.watchingOnly = true
return nil
}
// IsLocked returns whether or not the address managed is locked. When it is
// unlocked, the decryption key needed to decrypt private keys used for signing
// is in memory.
@ -1151,14 +1002,7 @@ func (m *Manager) isLocked() bool {
// Lock performs a best try effort to remove and zero all secret keys associated
// with the address manager.
//
// This function will return an error if invoked on a watching-only address
// manager.
func (m *Manager) Lock() error {
// A watching-only address manager can't be locked.
if m.watchingOnly {
return managerError(ErrWatchingOnly, errWatchingOnly, nil)
}
m.mtx.Lock()
defer m.mtx.Unlock()
@ -1177,14 +1021,7 @@ func (m *Manager) Lock() error {
// is stored in memory until the address manager is locked. Any failures that
// occur during this function will result in the address manager being locked,
// even if it was already unlocked prior to calling this function.
//
// This function will return an error if invoked on a watching-only address
// manager.
func (m *Manager) Unlock(ns walletdb.ReadBucket, passphrase []byte) error {
// A watching-only address manager can't be unlocked.
if m.watchingOnly {
return managerError(ErrWatchingOnly, errWatchingOnly, nil)
}
m.mtx.Lock()
defer m.mtx.Unlock()
@ -1337,7 +1174,7 @@ func (m *Manager) LookupAccount(ns walletdb.ReadBucket, name string) (KeyScope,
func (m *Manager) selectCryptoKey(keyType CryptoKeyType) (EncryptorDecryptor, error) {
if keyType == CKTPrivate || keyType == CKTScript {
// The manager must be unlocked to work with the private keys.
if m.locked || m.watchingOnly {
if m.locked {
return nil, managerError(ErrLocked, errLocked, nil)
}
}
@ -1401,7 +1238,7 @@ func newManager(chainParams *chaincfg.Params, masterKeyPub *snacl.SecretKey,
masterKeyPriv *snacl.SecretKey, cryptoKeyPub EncryptorDecryptor,
cryptoKeyPrivEncrypted, cryptoKeyScriptEncrypted []byte, syncInfo *syncState,
birthday time.Time, privPassphraseSalt [saltSize]byte,
scopedManagers map[KeyScope]*ScopedKeyManager, watchingOnly bool) *Manager {
scopedManagers map[KeyScope]*ScopedKeyManager) *Manager {
m := &Manager{
chainParams: chainParams,
@ -1419,7 +1256,6 @@ func newManager(chainParams *chaincfg.Params, masterKeyPub *snacl.SecretKey,
scopedManagers: scopedManagers,
externalAddrSchemas: make(map[AddressType][]KeyScope),
internalAddrSchemas: make(map[AddressType][]KeyScope),
watchingOnly: watchingOnly,
}
for _, sMgr := range m.scopedManagers {
@ -1546,12 +1382,6 @@ func loadManager(ns walletdb.ReadBucket, pubPassphrase []byte,
return nil, managerError(ErrUpgrade, str, nil)
}
// Load whether or not the manager is watching-only from the db.
watchingOnly, err := fetchWatchingOnly(ns)
if err != nil {
return nil, maybeConvertDbError(err)
}
// Load the master key params from the db.
masterKeyPubParams, masterKeyPrivParams, err := fetchMasterKeyParams(ns)
if err != nil {
@ -1579,15 +1409,13 @@ func loadManager(ns walletdb.ReadBucket, pubPassphrase []byte,
return nil, maybeConvertDbError(err)
}
// When not a watching-only manager, set the master private key params,
// but don't derive it now since the manager starts off locked.
// Set the master private key params, but don't derive it now since the
// manager starts off locked.
var masterKeyPriv snacl.SecretKey
if !watchingOnly {
err := masterKeyPriv.Unmarshal(masterKeyPrivParams)
if err != nil {
str := "failed to unmarshal master private key"
return nil, managerError(ErrCrypto, str, err)
}
err = masterKeyPriv.Unmarshal(masterKeyPrivParams)
if err != nil {
str := "failed to unmarshal master private key"
return nil, managerError(ErrCrypto, str, err)
}
// Derive the master public key using the serialized params and provided
@ -1654,8 +1482,7 @@ func loadManager(ns walletdb.ReadBucket, pubPassphrase []byte,
mgr := newManager(
chainParams, &masterKeyPub, &masterKeyPriv,
cryptoKeyPub, cryptoKeyPrivEnc, cryptoKeyScriptEnc, syncInfo,
birthday, privPassphraseSalt, scopedManagers, watchingOnly,
)
birthday, privPassphraseSalt, scopedManagers)
for _, scopedManager := range scopedManagers {
scopedManager.rootManager = mgr
@ -1797,11 +1624,6 @@ func createManagerKeyScope(ns walletdb.ReadWriteBucket,
// derived. This allows all chained addresses in the address manager
// to be recovered by using the same seed.
//
// If the provided seed value is nil the address manager will be
// created in watchingOnly mode in which case no default accounts or
// scoped managers are created - it is up to the caller to create a
// new one with NewAccountWatchingOnly and NewScopedKeyManager.
//
// All private and public keys and information are protected by secret
// keys derived from the provided private and public passphrases. The
// public passphrase is required on subsequent opens of the address
@ -1820,9 +1642,6 @@ func Create(ns walletdb.ReadWriteBucket, rootKey *hdkeychain.ExtendedKey,
chainParams *chaincfg.Params, config *ScryptOptions,
birthday time.Time) error {
// If the seed argument is nil we create in watchingOnly mode.
isWatchingOnly := rootKey == nil
// Return an error if the manager has already been created in
// the given database namespace.
exists := managerExists(ns)
@ -1831,17 +1650,13 @@ func Create(ns walletdb.ReadWriteBucket, rootKey *hdkeychain.ExtendedKey,
}
// Ensure the private passphrase is not empty.
if !isWatchingOnly && len(privPassphrase) == 0 {
if len(privPassphrase) == 0 {
str := "private passphrase may not be empty"
return managerError(ErrEmptyPassphrase, str, nil)
}
// Perform the initial bucket creation and database namespace setup.
defaultScopes := map[KeyScope]ScopeAddrSchema{}
if !isWatchingOnly {
defaultScopes = ScopeAddrMap
}
if err := createManagerNS(ns, defaultScopes); err != nil {
if err := createManagerNS(ns, ScopeAddrMap); err != nil {
return maybeConvertDbError(err)
}
@ -1890,92 +1705,90 @@ func Create(ns walletdb.ReadWriteBucket, rootKey *hdkeychain.ExtendedKey,
var masterKeyPriv *snacl.SecretKey
var cryptoKeyPrivEnc []byte
var cryptoKeyScriptEnc []byte
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()
if err != nil {
str := "failed to generate crypto private key"
return managerError(ErrCrypto, str, err)
}
defer cryptoKeyPriv.Zero()
cryptoKeyScript, err := newCryptoKey()
if err != nil {
str := "failed to generate crypto script key"
return managerError(ErrCrypto, str, err)
}
defer cryptoKeyScript.Zero()
cryptoKeyPrivEnc, err =
masterKeyPriv.Encrypt(cryptoKeyPriv.Bytes())
if err != nil {
str := "failed to encrypt crypto private key"
return managerError(ErrCrypto, str, err)
}
cryptoKeyScriptEnc, err =
masterKeyPriv.Encrypt(cryptoKeyScript.Bytes())
if err != nil {
str := "failed to encrypt crypto script key"
return managerError(ErrCrypto, str, err)
}
// Generate the BIP0044 HD key structure to ensure the
// provided seed can generate the required structure with no
// issues.
rootPubKey, err := rootKey.Neuter()
if err != nil {
str := "failed to neuter master extended key"
return managerError(ErrKeyChain, str, err)
}
// Next, for each registers default manager scope, we'll
// create the hardened cointype key for it, as well as the
// first default account.
for _, defaultScope := range DefaultKeyScopes {
err := createManagerKeyScope(
ns, defaultScope, rootKey, cryptoKeyPub, cryptoKeyPriv,
)
if err != nil {
return maybeConvertDbError(err)
}
}
// Before we proceed, we'll also store the root master private
// key within the database in an encrypted format. This is
// required as in the future, we may need to create additional
// scoped key managers.
masterHDPrivKeyEnc, err :=
cryptoKeyPriv.Encrypt([]byte(rootKey.String()))
if err != nil {
return maybeConvertDbError(err)
}
masterHDPubKeyEnc, err :=
cryptoKeyPub.Encrypt([]byte(rootPubKey.String()))
if err != nil {
return maybeConvertDbError(err)
}
err = putMasterHDKeys(ns, masterHDPrivKeyEnc, masterHDPubKeyEnc)
if err != nil {
return maybeConvertDbError(err)
}
privParams = masterKeyPriv.Marshal()
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()
if err != nil {
str := "failed to generate crypto private key"
return managerError(ErrCrypto, str, err)
}
defer cryptoKeyPriv.Zero()
cryptoKeyScript, err := newCryptoKey()
if err != nil {
str := "failed to generate crypto script key"
return managerError(ErrCrypto, str, err)
}
defer cryptoKeyScript.Zero()
cryptoKeyPrivEnc, err =
masterKeyPriv.Encrypt(cryptoKeyPriv.Bytes())
if err != nil {
str := "failed to encrypt crypto private key"
return managerError(ErrCrypto, str, err)
}
cryptoKeyScriptEnc, err =
masterKeyPriv.Encrypt(cryptoKeyScript.Bytes())
if err != nil {
str := "failed to encrypt crypto script key"
return managerError(ErrCrypto, str, err)
}
// Generate the BIP0044 HD key structure to ensure the
// provided seed can generate the required structure with no
// issues.
rootPubKey, err := rootKey.Neuter()
if err != nil {
str := "failed to neuter master extended key"
return managerError(ErrKeyChain, str, err)
}
// Next, for each registers default manager scope, we'll
// create the hardened cointype key for it, as well as the
// first default account.
for _, defaultScope := range DefaultKeyScopes {
err := createManagerKeyScope(
ns, defaultScope, rootKey, cryptoKeyPub, cryptoKeyPriv,
)
if err != nil {
return maybeConvertDbError(err)
}
}
// Before we proceed, we'll also store the root master private
// key within the database in an encrypted format. This is
// required as in the future, we may need to create additional
// scoped key managers.
masterHDPrivKeyEnc, err :=
cryptoKeyPriv.Encrypt([]byte(rootKey.String()))
if err != nil {
return maybeConvertDbError(err)
}
masterHDPubKeyEnc, err :=
cryptoKeyPub.Encrypt([]byte(rootPubKey.String()))
if err != nil {
return maybeConvertDbError(err)
}
err = putMasterHDKeys(ns, masterHDPrivKeyEnc, masterHDPubKeyEnc)
if err != nil {
return maybeConvertDbError(err)
}
privParams = masterKeyPriv.Marshal()
// Save the master key params to the database.
err = putMasterKeyParams(ns, pubParams, privParams)
@ -1990,13 +1803,6 @@ func Create(ns walletdb.ReadWriteBucket, rootKey *hdkeychain.ExtendedKey,
return maybeConvertDbError(err)
}
// Save the watching-only mode of the address manager to the
// database.
err = putWatchingOnly(ns, isWatchingOnly)
if err != nil {
return maybeConvertDbError(err)
}
// Save the initial synced to state.
err = PutSyncedTo(ns, &syncInfo.syncedTo)
if err != nil {

View file

@ -10,7 +10,6 @@ import (
"encoding/hex"
"errors"
"fmt"
"os"
"reflect"
"testing"
"time"
@ -70,7 +69,6 @@ type testContext struct {
internalAccount uint32
create bool
unlocked bool
watchingOnly bool
}
// addrType is the type of address being tested
@ -162,12 +160,6 @@ func testManagedPubKeyAddress(tc *testContext, prefix string,
// for the expected error when the manager is locked.
gotPrivKey, err := gotAddr.PrivKey()
switch {
case tc.watchingOnly:
// Confirm expected watching-only error.
testName := fmt.Sprintf("%s PrivKey", prefix)
if !checkManagerError(tc.t, testName, err, ErrWatchingOnly) {
return false
}
case tc.unlocked:
if err != nil {
tc.t.Errorf("%s PrivKey: unexpected error - got %v",
@ -194,12 +186,6 @@ func testManagedPubKeyAddress(tc *testContext, prefix string,
// the manager is locked.
gotWIF, err := gotAddr.ExportPrivKey()
switch {
case tc.watchingOnly:
// Confirm expected watching-only error.
testName := fmt.Sprintf("%s ExportPrivKey", prefix)
if !checkManagerError(tc.t, testName, err, ErrWatchingOnly) {
return false
}
case tc.unlocked:
if err != nil {
tc.t.Errorf("%s ExportPrivKey: unexpected error - "+
@ -245,13 +231,6 @@ func testManagedScriptAddress(tc *testContext, prefix string,
// the expected error when the manager is locked.
gotScript, err := gotAddr.Script()
switch {
case tc.watchingOnly && !wantAddr.scriptNotSecret:
// Confirm expected watching-only error.
testName := fmt.Sprintf("%s Script", prefix)
if !checkManagerError(tc.t, testName, err, ErrWatchingOnly) {
return false
}
// Either the manger is unlocked or the script is not considered to
// be secret and is encrypted with the public key.
case tc.unlocked || wantAddr.scriptNotSecret:
@ -448,13 +427,6 @@ func testExternalAddresses(tc *testContext) bool {
return false
}
// Everything after this point involves retesting with an unlocked
// address manager which is not possible for watching-only mode, so
// just exit now in that case.
if tc.watchingOnly {
return true
}
// Unlock the manager and retest all of the addresses to ensure the
// private information is valid as well.
err := walletdb.View(tc.db, func(tx walletdb.ReadTx) error {
@ -485,24 +457,19 @@ func testExternalAddresses(tc *testContext) bool {
// retrieved by Address, and that they work properly when the manager is locked
// and unlocked.
func testInternalAddresses(tc *testContext) bool {
// When the address manager is not in watching-only mode, unlocked it
// first to ensure that address generation works correctly when the
// address manager is unlocked and then locked later. These tests
// reverse the order done in the external tests which starts with a
// locked manager and unlock it afterwards.
if !tc.watchingOnly {
// Unlock the manager and retest all of the addresses to ensure the
// private information is valid as well.
err := walletdb.View(tc.db, func(tx walletdb.ReadTx) error {
ns := tx.ReadBucket(waddrmgrNamespaceKey)
return tc.rootManager.Unlock(ns, privPassphrase)
})
if err != nil {
tc.t.Errorf("Unlock: unexpected error: %v", err)
return false
}
tc.unlocked = true
// These tests reverse the order done in the external tests which starts
// with a locked manager and unlock it afterwards.
// Unlock the manager and retest all of the addresses to ensure the
// private information is valid as well.
err := walletdb.View(tc.db, func(tx walletdb.ReadTx) error {
ns := tx.ReadBucket(waddrmgrNamespaceKey)
return tc.rootManager.Unlock(ns, privPassphrase)
})
if err != nil {
tc.t.Errorf("Unlock: unexpected error: %v", err)
return false
}
tc.unlocked = true
prefix := testNamePrefix(tc) + " testInternalAddresses"
var addrs []ManagedAddress
@ -597,24 +564,10 @@ func testInternalAddresses(tc *testContext) bool {
return true
}
// The address manager could either be locked or unlocked here depending
// on whether or not it's a watching-only manager. When it's unlocked,
// this will test both the public and private address data are accurate.
// When it's locked, it must be watching-only, so only the public
// address information is tested and the private functions are checked
// to ensure they return the expected ErrWatchingOnly error.
if !testResults() {
return false
}
// Everything after this point involves locking the address manager and
// retesting the addresses with a locked manager. However, for
// watching-only mode, this has already happened, so just exit now in
// that case.
if tc.watchingOnly {
return true
}
// Lock the manager and retest all of the addresses to ensure the
// public information remains valid and the private functions return
// the expected error.
@ -639,55 +592,38 @@ func testLocking(tc *testContext) bool {
return false
}
// Locking an already lock manager should return an error. The error
// should be ErrLocked or ErrWatchingOnly depending on the type of the
// address manager.
// Locking an already lock manager should return an error.
err := tc.rootManager.Lock()
wantErrCode := ErrLocked
if tc.watchingOnly {
wantErrCode = ErrWatchingOnly
}
if !checkManagerError(tc.t, "Lock", err, wantErrCode) {
return false
}
// Ensure unlocking with the correct passphrase doesn't return any
// unexpected errors and the manager properly reports it is unlocked.
// Since watching-only address managers can't be unlocked, also ensure
// the correct error for that case.
err = walletdb.View(tc.db, func(tx walletdb.ReadTx) error {
ns := tx.ReadBucket(waddrmgrNamespaceKey)
return tc.rootManager.Unlock(ns, privPassphrase)
})
if tc.watchingOnly {
if !checkManagerError(tc.t, "Unlock", err, ErrWatchingOnly) {
return false
}
} else if err != nil {
if err != nil {
tc.t.Errorf("Unlock: unexpected error: %v", err)
return false
}
if !tc.watchingOnly && tc.rootManager.IsLocked() {
if tc.rootManager.IsLocked() {
tc.t.Error("IsLocked: returned true on unlocked manager")
return false
}
// Unlocking the manager again is allowed. Since watching-only address
// managers can't be unlocked, also ensure the correct error for that
// case.
// Unlocking the manager again is allowed.
err = walletdb.View(tc.db, func(tx walletdb.ReadTx) error {
ns := tx.ReadBucket(waddrmgrNamespaceKey)
return tc.rootManager.Unlock(ns, privPassphrase)
})
if tc.watchingOnly {
if !checkManagerError(tc.t, "Unlock2", err, ErrWatchingOnly) {
return false
}
} else if err != nil {
if err != nil {
tc.t.Errorf("Unlock: unexpected error: %v", err)
return false
}
if !tc.watchingOnly && tc.rootManager.IsLocked() {
if tc.rootManager.IsLocked() {
tc.t.Error("IsLocked: returned true on unlocked manager")
return false
}
@ -699,9 +635,6 @@ func testLocking(tc *testContext) bool {
return tc.rootManager.Unlock(ns, []byte("invalidpassphrase"))
})
wantErrCode = ErrWrongPassphrase
if tc.watchingOnly {
wantErrCode = ErrWatchingOnly
}
if !checkManagerError(tc.t, "Unlock", err, wantErrCode) {
return false
}
@ -760,19 +693,16 @@ func testImportPrivateKey(tc *testContext) bool {
},
}
// The manager must be unlocked to import a private key, however a
// watching-only manager can't be unlocked.
if !tc.watchingOnly {
err := walletdb.View(tc.db, func(tx walletdb.ReadTx) error {
ns := tx.ReadBucket(waddrmgrNamespaceKey)
return tc.rootManager.Unlock(ns, privPassphrase)
})
if err != nil {
tc.t.Errorf("Unlock: unexpected error: %v", err)
return false
}
tc.unlocked = true
// The manager must be unlocked to import a private key.
err := walletdb.View(tc.db, func(tx walletdb.ReadTx) error {
ns := tx.ReadBucket(waddrmgrNamespaceKey)
return tc.rootManager.Unlock(ns, privPassphrase)
})
if err != nil {
tc.t.Errorf("Unlock: unexpected error: %v", err)
return false
}
tc.unlocked = true
// Only import the private keys when in the create phase of testing.
tc.internalAccount = ImportedAddrAccount
@ -853,24 +783,10 @@ func testImportPrivateKey(tc *testContext) bool {
return !failed
}
// The address manager could either be locked or unlocked here depending
// on whether or not it's a watching-only manager. When it's unlocked,
// this will test both the public and private address data are accurate.
// When it's locked, it must be watching-only, so only the public
// address information is tested and the private functions are checked
// to ensure they return the expected ErrWatchingOnly error.
if !testResults() {
return false
}
// Everything after this point involves locking the address manager and
// retesting the addresses with a locked manager. However, for
// watching-only mode, this has already happened, so just exit now in
// that case.
if tc.watchingOnly {
return true
}
// Lock the manager and retest all of the addresses to ensure the
// private information returns the expected error.
if err := tc.rootManager.Lock(); err != nil {
@ -994,19 +910,16 @@ func testImportScript(tc *testContext) bool {
}
// The manager must be unlocked to import a private key and also for
// testing private data. However, a watching-only manager can't be
// unlocked.
if !tc.watchingOnly {
err := walletdb.View(tc.db, func(tx walletdb.ReadTx) error {
ns := tx.ReadBucket(waddrmgrNamespaceKey)
return tc.rootManager.Unlock(ns, privPassphrase)
})
if err != nil {
tc.t.Errorf("Unlock: unexpected error: %v", err)
return false
}
tc.unlocked = true
// testing private data.
err := walletdb.View(tc.db, func(tx walletdb.ReadTx) error {
ns := tx.ReadBucket(waddrmgrNamespaceKey)
return tc.rootManager.Unlock(ns, privPassphrase)
})
if err != nil {
tc.t.Errorf("Unlock: unexpected error: %v", err)
return false
}
tc.unlocked = true
// Only import the scripts when in the create phase of testing.
tc.internalAccount = ImportedAddrAccount
@ -1110,24 +1023,10 @@ func testImportScript(tc *testContext) bool {
return !failed
}
// The address manager could either be locked or unlocked here depending
// on whether or not it's a watching-only manager. When it's unlocked,
// this will test both the public and private address data are accurate.
// When it's locked, it must be watching-only, so only the public
// address information is tested and the private functions are checked
// to ensure they return the expected ErrWatchingOnly error.
if !testResults() {
return false
}
// Everything after this point involves locking the address manager and
// retesting the addresses with a locked manager. However, for
// watching-only mode, this has already happened, so just exit now in
// that case.
if tc.watchingOnly {
return true
}
// Lock the manager and retest all of the addresses to ensure the
// private information returns the expected error.
if err := tc.rootManager.Lock(); err != nil {
@ -1286,9 +1185,6 @@ func testChangePassphrase(tc *testContext) bool {
return false
}
// Attempt to change private passphrase with invalid old passphrase.
// The error should be ErrWrongPassphrase or ErrWatchingOnly depending
// on the type of the address manager.
testName = pfx + "ChangePassphrase (private) with invalid old passphrase"
err = walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) error {
ns := tx.ReadWriteBucket(waddrmgrNamespaceKey)
@ -1297,21 +1193,10 @@ func testChangePassphrase(tc *testContext) bool {
)
})
wantErrCode := ErrWrongPassphrase
if tc.watchingOnly {
wantErrCode = ErrWatchingOnly
}
if !checkManagerError(tc.t, testName, err, wantErrCode) {
return false
}
// Everything after this point involves testing that the private
// passphrase for the address manager can be changed successfully.
// This is not possible for watching-only mode, so just exit now in that
// case.
if tc.watchingOnly {
return true
}
// Change the private passphrase.
testName = pfx + "ChangePassphrase (private)"
err = walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) error {
@ -1368,22 +1253,6 @@ func testChangePassphrase(tc *testContext) bool {
// testNewAccount tests the new account creation func of the address manager works
// as expected.
func testNewAccount(tc *testContext) bool {
if tc.watchingOnly {
// Creating new accounts in watching-only mode should return ErrWatchingOnly
err := walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) error {
ns := tx.ReadWriteBucket(waddrmgrNamespaceKey)
_, err := tc.manager.NewAccount(ns, "test")
return err
})
if !checkManagerError(
tc.t, "Create account in watching-only mode", err,
ErrWatchingOnly,
) {
tc.manager.Close()
return false
}
return true
}
// Creating new accounts when wallet is locked should return ErrLocked
err := walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) error {
ns := tx.ReadWriteBucket(waddrmgrNamespaceKey)
@ -1711,148 +1580,26 @@ func testForEachAccountAddress(tc *testContext) bool {
// testManagerAPI tests the functions provided by the Manager API as well as
// the ManagedAddress, ManagedPubKeyAddress, and ManagedScriptAddress
// interfaces.
func testManagerAPI(tc *testContext, caseCreatedWatchingOnly bool) {
if !caseCreatedWatchingOnly {
// Test API for normal create (w/ seed) case.
testLocking(tc)
testExternalAddresses(tc)
testInternalAddresses(tc)
testImportPrivateKey(tc)
testImportScript(tc)
testMarkUsed(tc, true)
testChangePassphrase(tc)
func testManagerAPI(tc *testContext) {
// Test API for normal create (w/ seed) case.
testLocking(tc)
testExternalAddresses(tc)
testInternalAddresses(tc)
testImportPrivateKey(tc)
testImportScript(tc)
testMarkUsed(tc, true)
testChangePassphrase(tc)
// Reset default account
tc.internalAccount = 0
testNewAccount(tc)
testLookupAccount(tc)
testForEachAccount(tc)
testForEachAccountAddress(tc)
// Reset default account
tc.internalAccount = 0
testNewAccount(tc)
testLookupAccount(tc)
testForEachAccount(tc)
testForEachAccountAddress(tc)
// Rename account 1 "acct-create"
tc.internalAccount = 1
testRenameAccount(tc)
} else {
// Test API for created watch-only case.
testExternalAddresses(tc)
testInternalAddresses(tc)
testMarkUsed(tc, false)
testChangePassphrase(tc)
testNewAccount(tc)
expectedAccounts := map[string]uint32{
defaultAccountName: DefaultAccountNum,
}
testLookupExpectedAccount(tc, expectedAccounts, 0)
//testForEachAccount(tc)
testForEachAccountAddress(tc)
}
}
// testConvertWatchingOnly tests various facets of a watching-only address
// manager such as running the full set of API tests against a newly converted
// copy as well as when it is opened from an existing namespace.
func testConvertWatchingOnly(tc *testContext) bool {
// These tests check the case where the manager was not initially
// created watch-only, but converted to watch only ...
// Make a copy of the current database so the copy can be converted to
// watching only.
woMgrName := "mgrtestwo.bin"
_ = os.Remove(woMgrName)
fi, err := os.OpenFile(woMgrName, os.O_CREATE|os.O_RDWR, 0600)
if err != nil {
tc.t.Errorf("%v", err)
return false
}
if err := tc.db.Copy(fi); err != nil {
fi.Close()
tc.t.Errorf("%v", err)
return false
}
fi.Close()
defer os.Remove(woMgrName)
// Open the new database copy and get the address manager namespace.
db, err := walletdb.Open("bdb", woMgrName, true, defaultDBTimeout)
if err != nil {
tc.t.Errorf("openDbNamespace: unexpected error: %v", err)
return false
}
defer db.Close()
// Open the manager using the namespace and convert it to watching-only.
var mgr *Manager
err = walletdb.View(db, func(tx walletdb.ReadTx) error {
ns := tx.ReadBucket(waddrmgrNamespaceKey)
var err error
mgr, err = Open(ns, pubPassphrase, &chaincfg.MainNetParams)
return err
})
if err != nil {
tc.t.Errorf("%v", err)
return false
}
err = walletdb.Update(db, func(tx walletdb.ReadWriteTx) error {
ns := tx.ReadWriteBucket(waddrmgrNamespaceKey)
return mgr.ConvertToWatchingOnly(ns)
})
if err != nil {
tc.t.Errorf("%v", err)
return false
}
// Run all of the manager API tests against the converted manager and
// close it. We'll also retrieve the default scope (BIP0044) from the
// manager in order to use.
scopedMgr, err := mgr.FetchScopedKeyManager(KeyScopeBIP0044)
if err != nil {
tc.t.Errorf("unable to fetch bip 44 scope %v", err)
return false
}
testManagerAPI(&testContext{
t: tc.t,
caseName: tc.caseName,
db: db,
rootManager: mgr,
manager: scopedMgr,
internalAccount: 0,
create: false,
watchingOnly: true,
}, false)
mgr.Close()
// Open the watching-only manager and run all the tests again.
err = walletdb.View(db, func(tx walletdb.ReadTx) error {
ns := tx.ReadBucket(waddrmgrNamespaceKey)
var err error
mgr, err = Open(ns, pubPassphrase, &chaincfg.MainNetParams)
return err
})
if err != nil {
tc.t.Errorf("Open Watching-Only: unexpected error: %v", err)
return false
}
defer mgr.Close()
scopedMgr, err = mgr.FetchScopedKeyManager(KeyScopeBIP0044)
if err != nil {
tc.t.Errorf("unable to fetch bip 44 scope %v", err)
return false
}
testManagerAPI(&testContext{
t: tc.t,
caseName: tc.caseName,
db: db,
rootManager: mgr,
manager: scopedMgr,
internalAccount: 0,
create: false,
watchingOnly: true,
}, false)
return true
// Rename account 1 "acct-create"
tc.internalAccount = 1
testRenameAccount(tc)
}
// testSync tests various facets of setting the manager sync state.
@ -1914,57 +1661,43 @@ func testSync(tc *testContext) bool {
// much of the testing involves having specific state.
func _TestManager(t *testing.T) {
tests := []struct {
name string
createdWatchingOnly bool
rootKey *hdkeychain.ExtendedKey
privPassphrase []byte
name string
rootKey *hdkeychain.ExtendedKey
privPassphrase []byte
}{
{
name: "created with seed",
createdWatchingOnly: false,
rootKey: rootKey,
privPassphrase: privPassphrase,
},
{
name: "created watch-only",
createdWatchingOnly: true,
rootKey: nil,
privPassphrase: nil,
name: "created with seed",
rootKey: rootKey,
privPassphrase: privPassphrase,
},
}
for _, test := range tests {
// Need to wrap in a call so the defers work correctly.
testManagerCase(
t, test.name, test.createdWatchingOnly,
test.privPassphrase, test.rootKey,
)
testManagerCase(t, test.name, test.privPassphrase, test.rootKey)
}
}
func testManagerCase(t *testing.T, caseName string,
caseCreatedWatchingOnly bool, casePrivPassphrase []byte,
caseKey *hdkeychain.ExtendedKey) {
casePrivPassphrase []byte, caseKey *hdkeychain.ExtendedKey) {
teardown, db := emptyDB(t)
defer teardown()
if !caseCreatedWatchingOnly {
// Open manager that does not exist to ensure the expected error is
// returned.
err := walletdb.View(db, func(tx walletdb.ReadTx) error {
ns := tx.ReadBucket(waddrmgrNamespaceKey)
_, err := Open(ns, pubPassphrase, &chaincfg.MainNetParams)
return err
})
if !checkManagerError(t, "Open non-existent", err, ErrNoExist) {
return
}
// Open manager that does not exist to ensure the expected error is
// returned.
err := walletdb.View(db, func(tx walletdb.ReadTx) error {
ns := tx.ReadBucket(waddrmgrNamespaceKey)
_, err := Open(ns, pubPassphrase, &chaincfg.MainNetParams)
return err
})
if !checkManagerError(t, "Open non-existent", err, ErrNoExist) {
return
}
// Create a new manager.
var mgr *Manager
err := walletdb.Update(db, func(tx walletdb.ReadWriteTx) error {
err = walletdb.Update(db, func(tx walletdb.ReadWriteTx) error {
ns, err := tx.CreateTopLevelBucket(waddrmgrNamespaceKey)
if err != nil {
return err
@ -1981,10 +1714,6 @@ func testManagerCase(t *testing.T, caseName string,
return err
}
if caseCreatedWatchingOnly {
_, err = mgr.NewScopedKeyManager(
ns, KeyScopeBIP0044, ScopeAddrMap[KeyScopeBIP0044])
}
return err
})
if err != nil {
@ -2015,32 +1744,6 @@ func testManagerCase(t *testing.T, caseName string,
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, 0, nil,
)
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{
@ -2051,8 +1754,7 @@ func testManagerCase(t *testing.T, caseName string,
rootManager: mgr,
internalAccount: 0,
create: true,
watchingOnly: caseCreatedWatchingOnly,
}, caseCreatedWatchingOnly)
})
mgr.Close()
// Open the manager and run all the tests again in open mode which
@ -2081,29 +1783,20 @@ func testManagerCase(t *testing.T, caseName string,
rootManager: mgr,
internalAccount: 0,
create: false,
watchingOnly: caseCreatedWatchingOnly,
}
testManagerAPI(tc, caseCreatedWatchingOnly)
if !caseCreatedWatchingOnly {
// Now that the address manager has been tested in both the newly
// created and opened modes, test a watching-only version.
testConvertWatchingOnly(tc)
}
testManagerAPI(tc)
// Ensure that the manager sync state functionality works as expected.
testSync(tc)
if !caseCreatedWatchingOnly {
// Unlock the manager so it can be closed with it unlocked to ensure
// it works without issue.
err = walletdb.View(db, func(tx walletdb.ReadTx) error {
ns := tx.ReadBucket(waddrmgrNamespaceKey)
return mgr.Unlock(ns, casePrivPassphrase)
})
if err != nil {
t.Errorf("Unlock: unexpected error: %v", err)
}
// Unlock the manager so it can be closed with it unlocked to ensure
// it works without issue.
err = walletdb.View(db, func(tx walletdb.ReadTx) error {
ns := tx.ReadBucket(waddrmgrNamespaceKey)
return mgr.Unlock(ns, casePrivPassphrase)
})
if err != nil {
t.Errorf("Unlock: unexpected error: %v", err)
}
}
@ -2683,142 +2376,6 @@ func TestNewRawAccount(t *testing.T) {
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) {
teardown, db := emptyDB(t)
defer teardown()
t.Parallel()
// 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, 0, nil,
)
})
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) {
teardown, db := emptyDB(t)
defer teardown()
t.Parallel()
// 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, rootKey, 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, 0, nil,
)
})
if err != nil {
t.Fatalf("unable to create new account: %v", err)
}
testNewRawAccount(t, mgr, db, accountNum, scopedMgr)
}
func testNewRawAccount(t *testing.T, _ *Manager, db walletdb.DB,
accountNum uint32, scopedMgr *ScopedKeyManager) {
// With the account created, we should be able to derive new addresses

View file

@ -393,10 +393,7 @@ func (s *ScopedKeyManager) loadAccountInfo(ns walletdb.ReadBucket,
return hdkeychain.NewKeyFromString(string(serializedKey))
}
// The wallet will only contain private keys for default accounts if the
// wallet's not set up as watch-only and it's been unlocked.
watchOnly := s.rootManager.watchOnly()
hasPrivateKey := !s.rootManager.isLocked() && !watchOnly
hasPrivateKey := !s.rootManager.isLocked()
// Create the new account info with the known information. The rest of
// the fields are filled out below.
@ -435,29 +432,6 @@ func (s *ScopedKeyManager) loadAccountInfo(ns walletdb.ReadBucket,
}
}
case *dbWatchOnlyAccountRow:
acctInfo = &accountInfo{
acctName: row.name,
acctType: row.acctType,
nextExternalIndex: row.nextExternalIndex,
nextInternalIndex: row.nextInternalIndex,
addrSchema: row.addrSchema,
masterKeyFingerprint: row.masterKeyFingerprint,
}
// Use the crypto public key to decrypt the account public
// extended key.
acctInfo.acctKeyPub, err = decryptKey(
s.rootManager.cryptoKeyPub, row.pubKeyEncrypted,
)
if err != nil {
str := fmt.Sprintf("failed to decrypt public key for "+
"account %d", account)
return nil, managerError(ErrCrypto, str, err)
}
hasPrivateKey = false
default:
str := fmt.Sprintf("unsupported account type %T", row)
return nil, managerError(ErrDatabase, str, nil)
@ -546,8 +520,6 @@ func (s *ScopedKeyManager) AccountProperties(ns walletdb.ReadBucket,
props.InternalKeyCount = acctInfo.nextInternalIndex
props.AccountPubKey = acctInfo.acctKeyPub
props.MasterKeyFingerprint = acctInfo.masterKeyFingerprint
props.IsWatchOnly = s.rootManager.WatchOnly() ||
acctInfo.acctKeyPriv == nil
props.AddrSchema = acctInfo.addrSchema
// Export the account public key with the correct version
@ -574,7 +546,6 @@ func (s *ScopedKeyManager) AccountProperties(ns walletdb.ReadBucket,
}
} else {
props.AccountName = ImportedAddrAccountName // reserved, nonchangable
props.IsWatchOnly = s.rootManager.WatchOnly()
// Could be more efficient if this was tracked by the db.
var importedKeyCount uint32
@ -642,8 +613,7 @@ func (s *ScopedKeyManager) DeriveFromKeyPathCache(
)
}
watchOnly := s.rootManager.WatchOnly()
private := !s.rootManager.IsLocked() && !watchOnly
private := !s.rootManager.IsLocked()
// Now that we have the account information, we can derive the key
// directly.
@ -684,8 +654,7 @@ func (s *ScopedKeyManager) DeriveFromKeyPath(ns walletdb.ReadBucket,
s.mtx.Lock()
defer s.mtx.Unlock()
watchOnly := s.rootManager.WatchOnly()
private := !s.rootManager.IsLocked() && !watchOnly
private := !s.rootManager.IsLocked()
addrKey, _, _, err := s.deriveKeyFromPath(
ns, kp.InternalAccount, kp.Branch, kp.Index, private,
@ -739,7 +708,7 @@ func (s *ScopedKeyManager) chainAddressRowToManaged(ns walletdb.ReadBucket,
// Since the manger's mutex is assumed to held when invoking this
// function, we use the internal isLocked to avoid a deadlock.
private := !s.rootManager.isLocked() && !s.rootManager.watchOnly()
private := !s.rootManager.isLocked()
addressKey, acctKey, masterKeyFingerprint, err := s.deriveKeyFromPath(
ns, row.account, row.branch, row.index, private,
@ -981,8 +950,7 @@ func (s *ScopedKeyManager) nextAddresses(ns walletdb.ReadWriteBucket,
// Choose the account key to used based on whether the address manager
// is locked.
acctKey := acctInfo.acctKeyPub
watchOnly := s.rootManager.WatchOnly() || len(acctInfo.acctKeyEncrypted) == 0
if !s.rootManager.IsLocked() && !watchOnly {
if !s.rootManager.IsLocked() {
acctKey = acctInfo.acctKeyPriv
}
@ -1138,7 +1106,7 @@ func (s *ScopedKeyManager) nextAddresses(ns walletdb.ReadWriteBucket,
// Add the new managed address to the list of addresses
// that need their private keys derived when the
// address manager is next unlocked.
if s.rootManager.isLocked() && !watchOnly {
if s.rootManager.isLocked() {
s.deriveOnUnlock = append(s.deriveOnUnlock, info)
}
}
@ -1178,8 +1146,7 @@ func (s *ScopedKeyManager) extendAddresses(ns walletdb.ReadWriteBucket,
// Choose the account key to used based on whether the address manager
// is locked.
acctKey := acctInfo.acctKeyPub
watchOnly := s.rootManager.WatchOnly() || acctInfo.acctKeyPriv != nil
if !s.rootManager.IsLocked() && !watchOnly {
if !s.rootManager.IsLocked() {
acctKey = acctInfo.acctKeyPriv
}
@ -1328,7 +1295,7 @@ func (s *ScopedKeyManager) extendAddresses(ns walletdb.ReadWriteBucket,
// Add the new managed address to the list of addresses that
// need their private keys derived when the address manager is
// next unlocked.
if s.rootManager.IsLocked() && !watchOnly {
if s.rootManager.IsLocked() {
s.deriveOnUnlock = append(s.deriveOnUnlock, info)
}
}
@ -1489,9 +1456,6 @@ func (s *ScopedKeyManager) LastInternalAddress(ns walletdb.ReadBucket,
// number *directly*, rather than taking a string name for the account, then
// mapping that to the next highest account number.
func (s *ScopedKeyManager) NewRawAccount(ns walletdb.ReadWriteBucket, number uint32) error {
if s.rootManager.WatchOnly() {
return managerError(ErrWatchingOnly, errWatchingOnly, nil)
}
s.mtx.Lock()
defer s.mtx.Unlock()
@ -1507,45 +1471,12 @@ func (s *ScopedKeyManager) NewRawAccount(ns walletdb.ReadWriteBucket, number uin
return s.newAccount(ns, number, name)
}
// NewRawAccountWatchingOnly creates a new watching only account for the scoped
// manager. This method differs from the NewAccountWatchingOnly method in that
// this method takes the account number *directly*, rather than taking a string
// name for the account, then mapping that to the next highest account number.
//
// The master key fingerprint denotes the fingerprint of the root key
// corresponding to the account public key (also known as the key with
// derivation path m/). This may be required by some hardware wallets for proper
// identification and signing.
//
// An optional address schema may also be provided to override the
// ScopedKeyManager's address schema. This will affect all addresses derived
// from the account.
func (s *ScopedKeyManager) NewRawAccountWatchingOnly(
ns walletdb.ReadWriteBucket, number uint32,
pubKey *hdkeychain.ExtendedKey, masterKeyFingerprint uint32,
addrSchema *ScopeAddrSchema) 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, masterKeyFingerprint, addrSchema,
)
}
// NewAccount creates and returns a new account stored in the manager based on
// the given account name. If an account with the same name already exists,
// ErrDuplicateAccount will be returned. Since creating a new account requires
// access to the cointype keys (from which extended account keys are derived),
// it requires the manager to be unlocked.
func (s *ScopedKeyManager) NewAccount(ns walletdb.ReadWriteBucket, name string) (uint32, error) {
if s.rootManager.WatchOnly() {
return 0, managerError(ErrWatchingOnly, errWatchingOnly, nil)
}
s.mtx.Lock()
defer s.mtx.Unlock()
@ -1654,95 +1585,6 @@ func (s *ScopedKeyManager) newAccount(ns walletdb.ReadWriteBucket,
return putLastAccount(ns, &s.scope, account)
}
// NewAccountWatchingOnly is similar to NewAccount, but for watch-only wallets.
//
// The master key fingerprint denotes the fingerprint of the root key
// corresponding to the account public key (also known as the key with
// derivation path m/). This may be required by some hardware wallets for proper
// identification and signing.
//
// An optional address schema may also be provided to override the
// ScopedKeyManager's address schema. This will affect all addresses derived
// from the account.
func (s *ScopedKeyManager) NewAccountWatchingOnly(ns walletdb.ReadWriteBucket,
name string, pubKey *hdkeychain.ExtendedKey, masterKeyFingerprint uint32,
addrSchema *ScopeAddrSchema) (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.
err = s.newAccountWatchingOnly(
ns, account, name, pubKey, masterKeyFingerprint, addrSchema,
)
if err != nil {
return 0, err
}
return account, nil
}
// newAccountWatchingOnly is similar to newAccount, but for watching-only wallets.
//
// The master key fingerprint denotes the fingerprint of the root key
// corresponding to the account public key (also known as the key with
// derivation path m/). This may be required by some hardware wallets for proper
// identification and signing.
//
// An optional address schema may also be provided to override the
// ScopedKeyManager's address schema. This will affect all addresses derived
// from the account.
//
// 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,
masterKeyFingerprint uint32, addrSchema *ScopeAddrSchema) 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 = putWatchOnlyAccountInfo(
ns, &s.scope, account, acctPubEnc, masterKeyFingerprint, 0, 0,
name, addrSchema,
)
if err != nil {
return err
}
// Save last account metadata
return putLastAccount(ns, &s.scope, account)
}
// RenameAccount renames an account stored in the manager based on the given
// account number with the given name. If an account with the same name
// already exists, ErrDuplicateAccount will be returned.
@ -1796,21 +1638,6 @@ func (s *ScopedKeyManager) RenameAccount(ns walletdb.ReadWriteBucket,
return err
}
case *dbWatchOnlyAccountRow:
// Remove the old name key from the account name index.
if err = deleteAccountNameIndex(ns, &s.scope, row.name); err != nil {
return err
}
err = putWatchOnlyAccountInfo(
ns, &s.scope, account, row.pubKeyEncrypted,
row.masterKeyFingerprint, row.nextExternalIndex,
row.nextInternalIndex, name, row.addrSchema,
)
if err != nil {
return err
}
default:
str := fmt.Sprintf("unsupported account type %T", row)
return managerError(ErrDatabase, str, nil)
@ -1834,14 +1661,8 @@ func (s *ScopedKeyManager) RenameAccount(ns walletdb.ReadWriteBucket,
// All imported addresses will be part of the account defined by the
// ImportedAddrAccount constant.
//
// NOTE: When the address manager is watching-only, the private key itself will
// not be stored or available since it is private data. Instead, only the
// public key will be stored. This means it is paramount the private key is
// kept elsewhere as the watching-only address manager will NOT ever have access
// to it.
//
// This function will return an error if the address manager is locked and not
// watching-only, or not for the same network as the key trying to be imported.
// This function will return an error if the address manager is locked, or
// not for the same network as the key trying to be imported.
// It will also return an error if the address already exists. Any other
// errors returned are generally unexpected.
func (s *ScopedKeyManager) ImportPrivateKey(ns walletdb.ReadWriteBucket,
@ -1860,25 +1681,23 @@ func (s *ScopedKeyManager) ImportPrivateKey(ns walletdb.ReadWriteBucket,
defer s.mtx.Unlock()
// The manager must be unlocked to encrypt the imported private key.
if s.rootManager.IsLocked() && !s.rootManager.WatchOnly() {
if s.rootManager.IsLocked() {
return nil, managerError(ErrLocked, errLocked, nil)
}
// Encrypt the private key when not a watching-only address manager.
// Encrypt the private key.
var encryptedPrivKey []byte
if !s.rootManager.WatchOnly() {
privKeyBytes := wif.PrivKey.Serialize()
var err error
encryptedPrivKey, err = s.rootManager.cryptoKeyPriv.Encrypt(privKeyBytes)
zero.Bytes(privKeyBytes)
if err != nil {
str := fmt.Sprintf("failed to encrypt private key for %x",
wif.PrivKey.PubKey().SerializeCompressed())
return nil, managerError(ErrCrypto, str, err)
}
privKeyBytes := wif.PrivKey.Serialize()
var err error
encryptedPrivKey, err = s.rootManager.cryptoKeyPriv.Encrypt(privKeyBytes)
zero.Bytes(privKeyBytes)
if err != nil {
str := fmt.Sprintf("failed to encrypt private key for %x",
wif.PrivKey.PubKey().SerializeCompressed())
return nil, managerError(ErrCrypto, str, err)
}
err := s.importPublicKey(
err = s.importPublicKey(
ns, wif.SerializePubKey(), encryptedPrivKey,
s.addrSchema.ExternalAddrType, bs,
)
@ -1887,11 +1706,7 @@ func (s *ScopedKeyManager) ImportPrivateKey(ns walletdb.ReadWriteBucket,
}
// Create a new managed address based on the imported address.
if !s.rootManager.WatchOnly() {
return s.toImportedPrivateManagedAddress(wif)
}
pubKey := (*btcec.PublicKey)(&wif.PrivKey.PublicKey)
return s.toImportedPublicManagedAddress(pubKey, wif.CompressPubKey)
return s.toImportedPrivateManagedAddress(wif)
}
// ImportPublicKey imports a public key into the address manager.
@ -2051,12 +1866,8 @@ func (s *ScopedKeyManager) toImportedPublicManagedAddress(
// All imported script addresses will be part of the account defined by the
// ImportedAddrAccount constant.
//
// When the address manager is watching-only, the script itself will not be
// stored or available since it is considered private data.
//
// This function will return an error if the address manager is locked and not
// watching-only, or the address already exists. Any other errors returned are
// generally unexpected.
// This function will return an error if the address manager is locked, or the
// address already exists. Any other errors returned are generally unexpected.
func (s *ScopedKeyManager) ImportScript(ns walletdb.ReadWriteBucket,
script []byte, bs *BlockStamp) (ManagedScriptAddress, error) {
@ -2069,12 +1880,8 @@ func (s *ScopedKeyManager) ImportScript(ns walletdb.ReadWriteBucket,
// All imported script addresses will be part of the account defined by the
// ImportedAddrAccount constant.
//
// When the address manager is watching-only, the script itself will not be
// stored or available since it is considered private data.
//
// This function will return an error if the address manager is locked and not
// watching-only, or the address already exists. Any other errors returned are
// generally unexpected.
// This function will return an error if the address manager is locked, or the
// address already exists. Any other errors returned are generally unexpected.
func (s *ScopedKeyManager) ImportWitnessScript(ns walletdb.ReadWriteBucket,
script []byte, bs *BlockStamp, witnessVersion byte,
isSecretScript bool) (ManagedScriptAddress, error) {
@ -2099,13 +1906,6 @@ func (s *ScopedKeyManager) importScriptAddress(ns walletdb.ReadWriteBucket,
return nil, managerError(ErrLocked, errLocked, nil)
}
// A secret script can only be used with a non-watch only manager. If
// a wallet is watch-only then the script must be encrypted with the
// public encryption key.
if isSecretScript && s.rootManager.WatchOnly() {
return nil, managerError(ErrWatchingOnly, errWatchingOnly, nil)
}
// Witness script addresses use a SHA256.
var scriptHash []byte
switch addrType {
@ -2124,8 +1924,8 @@ func (s *ScopedKeyManager) importScriptAddress(ns walletdb.ReadWriteBucket,
return nil, managerError(ErrDuplicateAddress, str, nil)
}
// Encrypt the script hash using the crypto public key so it is
// accessible when the address manager is locked or watching-only.
// Encrypt the script hash using the crypto public key so it not
// accessible when the address manager is locked.
encryptedHash, err := s.rootManager.cryptoKeyPub.Encrypt(scriptHash)
if err != nil {
str := fmt.Sprintf("failed to encrypt script hash %x",
@ -2195,9 +1995,9 @@ func (s *ScopedKeyManager) importScriptAddress(ns walletdb.ReadWriteBucket,
}
// Create a new managed address based on the imported script. Also,
// when not a watching-only address manager, make a copy of the script
// since it will be cleared on lock and the script the caller passed
// should not be cleared out from under the caller.
// make a copy of the script since it will be cleared on lock and the
// script the caller passed // should not be cleared out from under the
// caller.
var (
managedAddr ManagedScriptAddress
baseScriptAddr *baseScriptAddress
@ -2390,22 +2190,6 @@ func (s *ScopedKeyManager) ForEachInternalActiveAddress(ns walletdb.ReadBucket,
return nil
}
// IsWatchOnlyAccount determines if the given account belonging to this scoped
// manager is set up as watch-only.
func (s *ScopedKeyManager) IsWatchOnlyAccount(ns walletdb.ReadBucket,
account uint32) (bool, error) {
s.mtx.Lock()
defer s.mtx.Unlock()
acctInfo, err := s.loadAccountInfo(ns, account)
if err != nil {
return false, err
}
return acctInfo.acctKeyPriv == nil, nil
}
// cloneKeyWithVersion clones an extended key to use the version corresponding
// to the manager's key scope. This should only be used for non-watch-only
// accounts as they are stored within the database using the legacy BIP-0044

View file

@ -197,40 +197,18 @@ func (w *Wallet) txToOutputs(outputs []*wire.TxOut, keyScope *waddrmgr.KeyScope,
return walletdb.ErrDryRunRollBack
}
// Before committing the transaction, we'll sign our inputs. If
// the inputs are part of a watch-only account, there's no
// private key information stored, so we'll skip signing such.
var watchOnly bool
if keyScope == nil {
// If a key scope wasn't specified, then coin selection
// was performed from the default wallet accounts
// (NP2WKH, P2WKH), so any key scope provided doesn't
// impact the result of this call.
watchOnly, err = w.Manager.IsWatchOnlyAccount(
addrmgrNs, waddrmgr.KeyScopeBIP0084, account,
)
} else {
watchOnly, err = w.Manager.IsWatchOnlyAccount(
addrmgrNs, *keyScope, account,
)
}
err = tx.AddAllInputScripts(
secretSource{w.Manager, addrmgrNs},
)
if err != nil {
return err
}
if !watchOnly {
err = tx.AddAllInputScripts(
secretSource{w.Manager, addrmgrNs},
)
if err != nil {
return err
}
err = validateMsgTx(
tx.Tx, tx.PrevScripts, tx.PrevInputValues,
)
if err != nil {
return err
}
err = validateMsgTx(
tx.Tx, tx.PrevScripts, tx.PrevInputValues,
)
if err != nil {
return err
}
if tx.ChangeIndex >= 0 && account == waddrmgr.ImportedAddrAccount {

View file

@ -8,8 +8,6 @@ import (
"github.com/lbryio/lbcd/chaincfg"
"github.com/lbryio/lbcutil/hdkeychain"
"github.com/lbryio/lbcwallet/waddrmgr"
"github.com/lbryio/lbcwallet/walletdb"
)
// defaultDBTimeout specifies the timeout value when opening the wallet
@ -53,48 +51,3 @@ func testWallet(t *testing.T) (*Wallet, func()) {
return w, cleanup
}
// testWalletWatchingOnly creates a test watch only wallet and unlocks it.
func testWalletWatchingOnly(t *testing.T) (*Wallet, func()) {
// Set up a wallet.
dir, err := ioutil.TempDir("", "test_wallet_watch_only")
if err != nil {
t.Fatalf("Failed to create db dir: %v", err)
}
cleanup := func() {
if err := os.RemoveAll(dir); err != nil {
t.Fatalf("could not cleanup test: %v", err)
}
}
pubPass := []byte("hello")
loader := NewLoader(
&chaincfg.TestNet3Params, dir, true, defaultDBTimeout, 250,
)
w, err := loader.CreateNewWatchingOnlyWallet(pubPass, time.Now())
if err != nil {
t.Fatalf("unable to create wallet: %v", err)
}
chainClient := &mockChainClient{}
w.chainClient = chainClient
err = walletdb.Update(w.Database(), func(tx walletdb.ReadWriteTx) error {
ns := tx.ReadWriteBucket(waddrmgrNamespaceKey)
for scope, schema := range waddrmgr.ScopeAddrMap {
_, err := w.Manager.NewScopedKeyManager(
ns, scope, schema,
)
if err != nil {
return err
}
}
return nil
})
if err != nil {
t.Fatalf("unable to create default scopes: %v", err)
}
return w, cleanup
}

View file

@ -268,12 +268,14 @@ func (w *Wallet) importAccountScope(ns walletdb.ReadWriteBucket, name string,
}
}
account, err := scopedMgr.NewAccountWatchingOnly(
ns, name, accountPubKey, masterKeyFingerprint, addrSchema,
)
if err != nil {
return nil, err
}
// FIXME
// account, err := scopedMgr.NewAccountWatchingOnly(
// ns, name, accountPubKey, masterKeyFingerprint, addrSchema,
// )
// if err != nil {
// return nil, err
// }
var account uint32
return scopedMgr.AccountProperties(ns, account)
}

View file

@ -137,23 +137,12 @@ func _TestImportAccount(t *testing.T) {
w, cleanup := testWallet(t)
defer cleanup()
testImportAccount(t, w, tc, false, tc.name)
})
name := tc.name + " watch-only"
t.Run(name, func(t *testing.T) {
t.Parallel()
w, cleanup := testWalletWatchingOnly(t)
defer cleanup()
testImportAccount(t, w, tc, true, name)
testImportAccount(t, w, tc, tc.name)
})
}
}
func testImportAccount(t *testing.T, w *Wallet, tc *testCase, watchOnly bool,
name string) {
func testImportAccount(t *testing.T, w *Wallet, tc *testCase, name string) {
// First derive the master public key of the account we want to import.
root, err := hdkeychain.NewKeyFromString(tc.masterPriv)
@ -208,9 +197,6 @@ func testImportAccount(t *testing.T, w *Wallet, tc *testCase, watchOnly bool,
// If the wallet is watch only, there is no default account and our
// imported account will be index 0.
firstAccountIndex := uint32(1)
if watchOnly {
firstAccountIndex = 0
}
// We should have 3 additional accounts now.
acctResult, err := w.Accounts(tc.expectedScope)
@ -220,7 +206,6 @@ func testImportAccount(t *testing.T, w *Wallet, tc *testCase, watchOnly bool,
// Validate the state of the accounts.
require.Equal(t, firstAccountIndex, acct1.AccountNumber)
require.Equal(t, name+"1", acct1.AccountName)
require.Equal(t, true, acct1.IsWatchOnly)
require.Equal(t, root.ParentFingerprint(), acct1.MasterKeyFingerprint)
require.NotNil(t, acct1.AccountPubKey)
require.Equal(t, acct1Pub.String(), acct1.AccountPubKey.String())
@ -230,7 +215,6 @@ func testImportAccount(t *testing.T, w *Wallet, tc *testCase, watchOnly bool,
require.Equal(t, firstAccountIndex+1, acct2.AccountNumber)
require.Equal(t, name+"2", acct2.AccountName)
require.Equal(t, true, acct2.IsWatchOnly)
require.Equal(t, root.ParentFingerprint(), acct2.MasterKeyFingerprint)
require.NotNil(t, acct2.AccountPubKey)
require.Equal(t, acct2Pub.String(), acct2.AccountPubKey.String())
@ -255,7 +239,6 @@ func testImportAccount(t *testing.T, w *Wallet, tc *testCase, watchOnly bool,
// Make sure we can't get private keys for the imported accounts.
_, err = w.DumpWIFPrivateKey(intAddr)
require.True(t, waddrmgr.IsError(err, waddrmgr.ErrWatchingOnly))
// Get the address info for the single key we imported.
switch tc.addrType {

View file

@ -168,9 +168,7 @@ func (l *Loader) CreateNewWallet(pubPassphrase, privPassphrase, seed []byte,
}
}
return l.createNewWallet(
pubPassphrase, privPassphrase, rootKey, bday, false,
)
return l.createNewWallet(pubPassphrase, privPassphrase, rootKey, bday)
}
// CreateNewWalletExtendedKey creates a new wallet from an extended master root
@ -180,25 +178,11 @@ func (l *Loader) CreateNewWallet(pubPassphrase, privPassphrase, seed []byte,
func (l *Loader) CreateNewWalletExtendedKey(pubPassphrase, privPassphrase []byte,
rootKey *hdkeychain.ExtendedKey, bday time.Time) (*Wallet, error) {
return l.createNewWallet(
pubPassphrase, privPassphrase, rootKey, 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,
)
return l.createNewWallet(pubPassphrase, privPassphrase, rootKey, bday)
}
func (l *Loader) createNewWallet(pubPassphrase, privPassphrase []byte,
rootKey *hdkeychain.ExtendedKey, bday time.Time,
isWatchingOnly bool) (*Wallet, error) {
rootKey *hdkeychain.ExtendedKey, bday time.Time) (*Wallet, error) {
defer l.mu.Unlock()
l.mu.Lock()
@ -232,22 +216,12 @@ func (l *Loader) createNewWallet(pubPassphrase, privPassphrase []byte,
}
// Initialize the newly created database for the wallet before opening.
if isWatchingOnly {
err := CreateWatchingOnlyWithCallback(
l.db, pubPassphrase, l.chainParams, bday,
l.walletCreated,
)
if err != nil {
return nil, err
}
} else {
err := CreateWithCallback(
l.db, pubPassphrase, privPassphrase, rootKey,
l.chainParams, bday, l.walletCreated,
)
if err != nil {
return nil, err
}
err = CreateWithCallback(
l.db, pubPassphrase, privPassphrase, rootKey,
l.chainParams, bday, l.walletCreated,
)
if err != nil {
return nil, err
}
// Open the newly-created wallet.

View file

@ -330,37 +330,6 @@ func (w *Wallet) FinalizePsbt(keyScope *waddrmgr.KeyScope, account uint32,
}
}
// Finally, if the input doesn't belong to a watch-only account,
// then we'll sign it as is, and populate the input with the
// witness and sigScript (if needed).
watchOnly := false
err = walletdb.View(w.db, func(tx walletdb.ReadTx) error {
ns := tx.ReadBucket(waddrmgrNamespaceKey)
var err error
if keyScope == nil {
// If a key scope wasn't specified, then coin
// selection was performed from the default
// wallet accounts (NP2WKH, P2WKH), so any key
// scope provided doesn't impact the result of
// this call.
watchOnly, err = w.Manager.IsWatchOnlyAccount(
ns, waddrmgr.KeyScopeBIP0084, account,
)
} else {
watchOnly, err = w.Manager.IsWatchOnlyAccount(
ns, *keyScope, account,
)
}
return err
})
if err != nil {
return fmt.Errorf("unable to determine if account is "+
"watch-only: %v", err)
}
if watchOnly {
continue
}
witness, sigScript, err := w.ComputeInputScript(
tx, signOutput, idx, sigHashes, in.SighashType, nil,
)

View file

@ -1177,16 +1177,14 @@ out:
// private key material, we need to prevent it from
// doing so while we are assembling the transaction.
release := func() {}
if !w.Manager.WatchOnly() {
heldUnlock, err := w.holdUnlock()
if err != nil {
txr.resp <- createTxResponse{nil, err}
continue
}
release = heldUnlock.release
heldUnlock, err := w.holdUnlock()
if err != nil {
txr.resp <- createTxResponse{nil, err}
continue
}
release = heldUnlock.release
tx, err := w.txToOutputs(
txr.outputs, txr.keyScope, txr.account,
txr.minconf, txr.feeSatPerKB,
@ -2640,10 +2638,11 @@ func (w *Wallet) ListUnspent(minconf, maxconf int32,
//
// TODO: Each case will need updates when watch-only addrs
// is added. For P2PK, P2PKH, and P2SH, the address must be
// looked up and not be watching-only. For multisig, all
// pubkeys must belong to the manager with the associated
// private key (currently it only checks whether the pubkey
// exists, since the private key is required at the moment).
// looked up.
// For multisig, all pubkeys must belong to the manager with
// the associated private key (currently it only checks whether
// the pubkey exists, since the private key is required at the
// moment).
var spendable bool
scSwitch:
switch sc {
@ -3199,13 +3198,6 @@ func (w *Wallet) SendOutputs(outputs []*wire.TxOut, keyScope *waddrmgr.KeyScope,
return nil, err
}
// If our wallet is read-only, we'll get a transaction with coins
// selected but no witness data. In such a case we need to inform our
// caller that they'll actually need to go ahead and sign the TX.
if w.Manager.WatchOnly() {
return createdTx.Tx, ErrTxUnsigned
}
txHash, err := w.reliablyPublishTransaction(createdTx.Tx, label)
if err != nil {
return nil, err
@ -3695,19 +3687,7 @@ func CreateWithCallback(db walletdb.DB, pubPass, privPass []byte,
birthday time.Time, cb func(walletdb.ReadWriteTx) error) error {
return create(
db, pubPass, privPass, rootKey, params, birthday, false, cb,
)
}
// CreateWatchingOnlyWithCallback is the same as CreateWatchingOnly with an
// added callback that will be called in the same transaction the wallet
// structure is initialized.
func CreateWatchingOnlyWithCallback(db walletdb.DB, pubPass []byte,
params *chaincfg.Params, birthday time.Time,
cb func(walletdb.ReadWriteTx) error) error {
return create(
db, pubPass, nil, nil, params, birthday, true, cb,
db, pubPass, privPass, rootKey, params, birthday, cb,
)
}
@ -3719,31 +3699,16 @@ func Create(db walletdb.DB, pubPass, privPass []byte,
birthday time.Time) error {
return create(
db, pubPass, privPass, rootKey, params, birthday, false, nil,
db, pubPass, privPass, rootKey, params, birthday, nil,
)
}
// CreateWatchingOnly creates an new watch-only wallet, writing it to
// an empty database. No root key can be provided as this wallet will be
// watching only. Likewise no private passphrase may be provided
// either.
func CreateWatchingOnly(db walletdb.DB, pubPass []byte,
params *chaincfg.Params, birthday time.Time) error {
return create(
db, pubPass, nil, nil, params, birthday, true, nil,
)
}
func create(db walletdb.DB, pubPass, privPass []byte,
rootKey *hdkeychain.ExtendedKey, params *chaincfg.Params,
birthday time.Time, isWatchingOnly bool,
birthday time.Time,
cb func(walletdb.ReadWriteTx) error) error {
// If no root key was provided, we create one now from a random seed.
// But only if this is not a watching-only wallet where the accounts are
// created individually from their xpubs.
if !isWatchingOnly && rootKey == nil {
if rootKey == nil {
hdSeed, err := hdkeychain.GenerateSeed(
hdkeychain.RecommendedSeedLen,
)
@ -3760,7 +3725,7 @@ func create(db walletdb.DB, pubPass, privPass []byte,
}
// We need a private key if this isn't a watching only wallet.
if !isWatchingOnly && rootKey != nil && !rootKey.IsPrivate() {
if rootKey != nil && !rootKey.IsPrivate() {
return fmt.Errorf("need extended private key for wallet that " +
"is not watching only")
}

View file

@ -1,36 +0,0 @@
// 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/lbryio/lbcd/chaincfg"
_ "github.com/lbryio/lbcwallet/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, defaultDBTimeout, 250,
)
_, err = loader.CreateNewWatchingOnlyWallet(pubPass, time.Now())
if err != nil {
t.Fatalf("unable to create wallet: %v", err)
}
}