mirror of
https://github.com/LBRYFoundation/lbcwallet.git
synced 2025-08-23 17:47:29 +00:00
consolidate: remove watch-only address/account support
This commit is contained in:
parent
2b0d245b1f
commit
de408d4133
19 changed files with 315 additions and 1688 deletions
|
@ -422,12 +422,6 @@ var helpDescsEnUS = map[string]string{
|
||||||
"The wallet must be unlocked for this request to succeed.",
|
"The wallet must be unlocked for this request to succeed.",
|
||||||
"createnewaccount-account": "Name of the new account",
|
"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.
|
// GetBestBlockCmd help.
|
||||||
"getbestblock--synopsis": "Returns the hash and height of the newest block in the best chain that wallet has finished syncing with.",
|
"getbestblock--synopsis": "Returns the hash and height of the newest block in the best chain that wallet has finished syncing with.",
|
||||||
|
|
||||||
|
|
|
@ -63,7 +63,6 @@ var Methods = []struct {
|
||||||
{"walletpassphrase", nil},
|
{"walletpassphrase", nil},
|
||||||
{"walletpassphrasechange", nil},
|
{"walletpassphrasechange", nil},
|
||||||
{"createnewaccount", nil},
|
{"createnewaccount", nil},
|
||||||
{"exportwatchingwallet", returnsString},
|
|
||||||
{"getbestblock", []interface{}{(*btcjson.GetBestBlockResult)(nil)}},
|
{"getbestblock", []interface{}{(*btcjson.GetBestBlockResult)(nil)}},
|
||||||
{"getunconfirmedbalance", returnsNumber},
|
{"getunconfirmedbalance", returnsNumber},
|
||||||
{"listaddresstransactions", returnsLTRArray},
|
{"listaddresstransactions", returnsLTRArray},
|
||||||
|
|
|
@ -506,9 +506,6 @@ func getAddressInfo(icmd interface{}, w *wallet.Wallet) (interface{}, error) {
|
||||||
// just break out now if there is an error.
|
// just break out now if there is an error.
|
||||||
script, err := ma.Script()
|
script, err := ma.Script()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if waddrmgr.IsError(err, waddrmgr.ErrWatchingOnly) {
|
|
||||||
result.IsWatchOnly = true
|
|
||||||
}
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
hexScript := hex.EncodeToString(script)
|
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
|
// walletLock handles a walletlock request by locking the all account
|
||||||
// wallets, returning an error if any wallet is not encrypted (for example,
|
// wallets, returning an error if any wallet is not encrypted.
|
||||||
// a watching-only wallet).
|
|
||||||
func walletLock(icmd interface{}, w *wallet.Wallet) (interface{}, error) {
|
func walletLock(icmd interface{}, w *wallet.Wallet) (interface{}, error) {
|
||||||
w.Lock()
|
w.Lock()
|
||||||
return nil, nil
|
return nil, nil
|
||||||
|
|
|
@ -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",
|
"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",
|
"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",
|
"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",
|
"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",
|
"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",
|
"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,
|
"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"
|
||||||
|
|
|
@ -101,8 +101,7 @@ type ManagedPubKeyAddress interface {
|
||||||
ExportPubKey() string
|
ExportPubKey() string
|
||||||
|
|
||||||
// PrivKey returns the private key for the address. It can fail if the
|
// 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
|
// address manager is locked, or the address does not have any keys.
|
||||||
// have any keys.
|
|
||||||
PrivKey() (*btcec.PrivateKey, error)
|
PrivKey() (*btcec.PrivateKey, error)
|
||||||
|
|
||||||
// ExportPrivKey returns the private key associated with the address
|
// ExportPrivKey returns the private key associated with the address
|
||||||
|
@ -155,12 +154,6 @@ func (a *managedAddress) unlock(key EncryptorDecryptor) ([]byte, error) {
|
||||||
a.privKeyMutex.Lock()
|
a.privKeyMutex.Lock()
|
||||||
defer a.privKeyMutex.Unlock()
|
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 {
|
if len(a.privKeyCT) == 0 {
|
||||||
privKey, err := key.Decrypt(a.privKeyEncrypted)
|
privKey, err := key.Decrypt(a.privKeyEncrypted)
|
||||||
if err != nil {
|
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
|
// 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.
|
// This is part of the ManagedPubKeyAddress interface implementation.
|
||||||
func (a *managedAddress) PrivKey() (*btcec.PrivateKey, error) {
|
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()
|
a.manager.mtx.Lock()
|
||||||
defer a.manager.mtx.Unlock()
|
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.
|
// This is part of the ManagedAddress interface implementation.
|
||||||
func (a *scriptAddress) Script() ([]byte, error) {
|
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()
|
a.manager.mtx.Lock()
|
||||||
defer a.manager.mtx.Unlock()
|
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.
|
// This is part of the ManagedAddress interface implementation.
|
||||||
func (a *witnessScriptAddress) Script() ([]byte, error) {
|
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()
|
a.manager.mtx.Lock()
|
||||||
defer a.manager.mtx.Unlock()
|
defer a.manager.mtx.Unlock()
|
||||||
|
|
270
waddrmgr/db.go
270
waddrmgr/db.go
|
@ -6,7 +6,6 @@
|
||||||
package waddrmgr
|
package waddrmgr
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"errors"
|
"errors"
|
||||||
|
@ -85,12 +84,6 @@ const (
|
||||||
// database. This is an account that re-uses the key derivation schema
|
// database. This is an account that re-uses the key derivation schema
|
||||||
// of BIP0044-like accounts.
|
// of BIP0044-like accounts.
|
||||||
accountDefault accountType = 0 // not iota as they need to be stable
|
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.
|
// dbAccountRow houses information stored about an account in the database.
|
||||||
|
@ -110,18 +103,6 @@ type dbDefaultAccountRow struct {
|
||||||
name string
|
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
|
// dbAddressRow houses common information stored about an address in the
|
||||||
// database.
|
// database.
|
||||||
type dbAddressRow struct {
|
type dbAddressRow struct {
|
||||||
|
@ -298,7 +279,6 @@ var (
|
||||||
cryptoPrivKeyName = []byte("cpriv")
|
cryptoPrivKeyName = []byte("cpriv")
|
||||||
cryptoPubKeyName = []byte("cpub")
|
cryptoPubKeyName = []byte("cpub")
|
||||||
cryptoScriptKeyName = []byte("cscript")
|
cryptoScriptKeyName = []byte("cscript")
|
||||||
watchingOnlyName = []byte("watchonly")
|
|
||||||
|
|
||||||
// Sync related key names (sync bucket).
|
// Sync related key names (sync bucket).
|
||||||
syncedToName = []byte("syncedto")
|
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
|
// fetchMasterKeyParams loads the master key parameters needed to derive them
|
||||||
// (when given the correct user-supplied passphrase) from the database. Either
|
// (when given the correct user-supplied passphrase) from the database.
|
||||||
// returned value can be nil, but in practice only the private key params will
|
|
||||||
// be nil for a watching-only database.
|
|
||||||
func fetchMasterKeyParams(ns walletdb.ReadBucket) ([]byte, []byte, error) {
|
func fetchMasterKeyParams(ns walletdb.ReadBucket) ([]byte, []byte, error) {
|
||||||
bucket := ns.NestedReadBucket(mainBucketName)
|
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
|
// fetchCryptoKeys loads the encrypted crypto keys which are in turn used to
|
||||||
// protect the extended keys, imported keys, and scripts. Any of the returned
|
// 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
|
// values can be nil
|
||||||
// will be nil for a watching-only database.
|
|
||||||
func fetchCryptoKeys(ns walletdb.ReadBucket) ([]byte, []byte, []byte, error) {
|
func fetchCryptoKeys(ns walletdb.ReadBucket) ([]byte, []byte, []byte, error) {
|
||||||
bucket := ns.NestedReadBucket(mainBucketName)
|
bucket := ns.NestedReadBucket(mainBucketName)
|
||||||
|
|
||||||
|
@ -672,35 +649,6 @@ func putCryptoKeys(ns walletdb.ReadWriteBucket, pubKeyEncrypted, privKeyEncrypte
|
||||||
return nil
|
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.
|
// deserializeAccountRow deserializes the passed serialized account information.
|
||||||
// This is used as a common base for the various account types to deserialize
|
// This is used as a common base for the various account types to deserialize
|
||||||
// the common parts.
|
// the common parts.
|
||||||
|
@ -817,159 +765,6 @@ func serializeDefaultAccountRow(encryptedPubKey, encryptedPrivKey []byte,
|
||||||
return rawData
|
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
|
// forEachKeyScope calls the given function for each known manager scope
|
||||||
// within the set of scopes known by the root manager.
|
// within the set of scopes known by the root manager.
|
||||||
func forEachKeyScope(ns walletdb.ReadBucket, fn func(KeyScope) error) error {
|
func forEachKeyScope(ns walletdb.ReadBucket, fn func(KeyScope) error) error {
|
||||||
|
@ -1108,8 +903,6 @@ func fetchAccountInfo(ns walletdb.ReadBucket, scope *KeyScope,
|
||||||
switch row.acctType {
|
switch row.acctType {
|
||||||
case accountDefault:
|
case accountDefault:
|
||||||
return deserializeDefaultAccountRow(accountID, row)
|
return deserializeDefaultAccountRow(accountID, row)
|
||||||
case accountWatchOnly:
|
|
||||||
return deserializeWatchOnlyAccountRow(accountID, row)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
str := fmt.Sprintf("unsupported account type '%d'", row.acctType)
|
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)
|
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.
|
// putAccountInfo stores the provided account information to the database.
|
||||||
func putAccountInfo(ns walletdb.ReadWriteBucket, scope *KeyScope,
|
func putAccountInfo(ns walletdb.ReadWriteBucket, scope *KeyScope,
|
||||||
account uint32, acctRow *dbAccountRow, name string) error {
|
account uint32, acctRow *dbAccountRow, name string) error {
|
||||||
|
@ -1763,32 +1532,6 @@ func putChainedAddress(ns walletdb.ReadWriteBucket, scope *KeyScope,
|
||||||
arow.pubKeyEncrypted, arow.privKeyEncrypted,
|
arow.pubKeyEncrypted, arow.privKeyEncrypted,
|
||||||
nextExternalIndex, nextInternalIndex, arow.name,
|
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))
|
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.
|
// 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 {
|
func deletePrivateKeys(ns walletdb.ReadWriteBucket) error {
|
||||||
bucket := ns.NestedReadWriteBucket(mainBucketName)
|
bucket := ns.NestedReadWriteBucket(mainBucketName)
|
||||||
|
|
||||||
|
@ -2058,9 +1795,6 @@ func deletePrivateKeys(ns walletdb.ReadWriteBucket) error {
|
||||||
str := "failed to delete account private key"
|
str := "failed to delete account private key"
|
||||||
return managerError(ErrDatabase, str, err)
|
return managerError(ErrDatabase, str, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Watch-only accounts don't contain any private keys.
|
|
||||||
case accountWatchOnly:
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -29,10 +29,6 @@ var (
|
||||||
// errLocked is the common error description used for the ErrLocked
|
// errLocked is the common error description used for the ErrLocked
|
||||||
// error code.
|
// error code.
|
||||||
errLocked = "address manager is locked"
|
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.
|
// ErrorCode identifies a kind of error.
|
||||||
|
@ -86,11 +82,6 @@ const (
|
||||||
// manager to be unlocked, was requested on a locked account manager.
|
// manager to be unlocked, was requested on a locked account manager.
|
||||||
ErrLocked
|
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 indicates that the requested account is not valid.
|
||||||
ErrInvalidAccount
|
ErrInvalidAccount
|
||||||
|
|
||||||
|
@ -157,7 +148,6 @@ var errorCodeStrings = map[ErrorCode]string{
|
||||||
ErrCoinTypeTooHigh: "ErrCoinTypeTooHigh",
|
ErrCoinTypeTooHigh: "ErrCoinTypeTooHigh",
|
||||||
ErrAccountNumTooHigh: "ErrAccountNumTooHigh",
|
ErrAccountNumTooHigh: "ErrAccountNumTooHigh",
|
||||||
ErrLocked: "ErrLocked",
|
ErrLocked: "ErrLocked",
|
||||||
ErrWatchingOnly: "ErrWatchingOnly",
|
|
||||||
ErrInvalidAccount: "ErrInvalidAccount",
|
ErrInvalidAccount: "ErrInvalidAccount",
|
||||||
ErrAddressNotFound: "ErrAddressNotFound",
|
ErrAddressNotFound: "ErrAddressNotFound",
|
||||||
ErrAccountNotFound: "ErrAccountNotFound",
|
ErrAccountNotFound: "ErrAccountNotFound",
|
||||||
|
|
|
@ -28,7 +28,6 @@ func TestErrorCodeStringer(t *testing.T) {
|
||||||
{waddrmgr.ErrCoinTypeTooHigh, "ErrCoinTypeTooHigh"},
|
{waddrmgr.ErrCoinTypeTooHigh, "ErrCoinTypeTooHigh"},
|
||||||
{waddrmgr.ErrAccountNumTooHigh, "ErrAccountNumTooHigh"},
|
{waddrmgr.ErrAccountNumTooHigh, "ErrAccountNumTooHigh"},
|
||||||
{waddrmgr.ErrLocked, "ErrLocked"},
|
{waddrmgr.ErrLocked, "ErrLocked"},
|
||||||
{waddrmgr.ErrWatchingOnly, "ErrWatchingOnly"},
|
|
||||||
{waddrmgr.ErrInvalidAccount, "ErrInvalidAccount"},
|
{waddrmgr.ErrInvalidAccount, "ErrInvalidAccount"},
|
||||||
{waddrmgr.ErrAddressNotFound, "ErrAddressNotFound"},
|
{waddrmgr.ErrAddressNotFound, "ErrAddressNotFound"},
|
||||||
{waddrmgr.ErrAccountNotFound, "ErrAccountNotFound"},
|
{waddrmgr.ErrAccountNotFound, "ErrAccountNotFound"},
|
||||||
|
|
|
@ -42,16 +42,6 @@ const (
|
||||||
// ImportedAddrAccountName is the name of the imported account.
|
// ImportedAddrAccountName is the name of the imported account.
|
||||||
ImportedAddrAccountName = "imported"
|
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 is the number of the default account.
|
||||||
DefaultAccountNum = 0
|
DefaultAccountNum = 0
|
||||||
|
|
||||||
|
@ -221,10 +211,6 @@ type AccountProperties struct {
|
||||||
// KeyScope is the key scope the account belongs to.
|
// KeyScope is the key scope the account belongs to.
|
||||||
KeyScope KeyScope
|
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
|
// AddrSchema, if non-nil, specifies an address schema override for
|
||||||
// address generation only applicable to the account.
|
// address generation only applicable to the account.
|
||||||
AddrSchema *ScopeAddrSchema
|
AddrSchema *ScopeAddrSchema
|
||||||
|
@ -352,12 +338,11 @@ type Manager struct {
|
||||||
externalAddrSchemas map[AddressType][]KeyScope
|
externalAddrSchemas map[AddressType][]KeyScope
|
||||||
internalAddrSchemas map[AddressType][]KeyScope
|
internalAddrSchemas map[AddressType][]KeyScope
|
||||||
|
|
||||||
syncState syncState
|
syncState syncState
|
||||||
watchingOnly bool
|
birthday time.Time
|
||||||
birthday time.Time
|
locked bool
|
||||||
locked bool
|
closed bool
|
||||||
closed bool
|
chainParams *chaincfg.Params
|
||||||
chainParams *chaincfg.Params
|
|
||||||
|
|
||||||
// masterKeyPub is the secret key used to secure the cryptoKeyPub key
|
// masterKeyPub is the secret key used to secure the cryptoKeyPub key
|
||||||
// and masterKeyPriv is the secret key used to secure the cryptoKeyPriv
|
// and masterKeyPriv is the secret key used to secure the cryptoKeyPriv
|
||||||
|
@ -397,46 +382,6 @@ type Manager struct {
|
||||||
hashedPrivPassphrase [sha512.Size]byte
|
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
|
// lock performs a best try effort to remove and zero all secret keys associated
|
||||||
// with the address manager.
|
// with the address manager.
|
||||||
//
|
//
|
||||||
|
@ -497,7 +442,7 @@ func (m *Manager) Close() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attempt to clear private key material from memory.
|
// Attempt to clear private key material from memory.
|
||||||
if !m.watchingOnly && !m.locked {
|
if !m.locked {
|
||||||
m.lock()
|
m.lock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -525,51 +470,50 @@ func (m *Manager) NewScopedKeyManager(ns walletdb.ReadWriteBucket,
|
||||||
defer m.mtx.Unlock()
|
defer m.mtx.Unlock()
|
||||||
|
|
||||||
var rootPriv *hdkeychain.ExtendedKey
|
var rootPriv *hdkeychain.ExtendedKey
|
||||||
if !m.watchingOnly {
|
// If the manager is locked, then we can't create a new scoped
|
||||||
// If the manager is locked, then we can't create a new scoped
|
// manager.
|
||||||
// manager.
|
if m.locked {
|
||||||
if m.locked {
|
return nil, managerError(ErrLocked, errLocked, nil)
|
||||||
return nil, managerError(ErrLocked, errLocked, nil)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Now that we know the manager is unlocked, we'll need to
|
// Now that we know the manager is unlocked, we'll need to
|
||||||
// fetch the root master HD private key. This is required as
|
// fetch the root master HD private key. This is required as
|
||||||
// we'll be attempting the following derivation:
|
// we'll be attempting the following derivation:
|
||||||
// m/purpose'/cointype'
|
// m/purpose'/cointype'
|
||||||
//
|
//
|
||||||
// Note that the path to the coin type is requires hardened
|
// Note that the path to the coin type is requires hardened
|
||||||
// derivation, therefore this can only be done if the wallet's
|
// derivation, therefore this can only be done if the wallet's
|
||||||
// root key hasn't been neutered.
|
// root key hasn't been neutered.
|
||||||
masterRootPrivEnc, _ := fetchMasterHDKeys(ns)
|
masterRootPrivEnc, _ := fetchMasterHDKeys(ns)
|
||||||
|
|
||||||
// If the master root private key isn't found within the
|
// If the master root private key isn't found within the
|
||||||
// database, but we need to bail here as we can't create the
|
// database, but we need to bail here as we can't create the
|
||||||
// cointype key without the master root private key.
|
// cointype key without the master root private key.
|
||||||
if masterRootPrivEnc == nil {
|
if masterRootPrivEnc == nil {
|
||||||
return nil, managerError(ErrWatchingOnly, "", 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
|
// Before we can derive any new scoped managers using this
|
||||||
// key, we'll need to fully decrypt it.
|
// key, we'll need to fully decrypt it.
|
||||||
serializedMasterRootPriv, err :=
|
serializedMasterRootPriv, err :=
|
||||||
m.cryptoKeyPriv.Decrypt(masterRootPrivEnc)
|
m.cryptoKeyPriv.Decrypt(masterRootPrivEnc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
str := fmt.Sprintf("failed to decrypt master root " +
|
str := fmt.Sprintf("failed to decrypt master root " +
|
||||||
"serialized private key")
|
"serialized private key")
|
||||||
return nil, managerError(ErrLocked, str, err)
|
return nil, managerError(ErrLocked, str, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now that we know the root priv is within the database,
|
// Now that we know the root priv is within the database,
|
||||||
// we'll decode it into a usable object.
|
// we'll decode it into a usable object.
|
||||||
rootPriv, err = hdkeychain.NewKeyFromString(
|
rootPriv, err = hdkeychain.NewKeyFromString(
|
||||||
string(serializedMasterRootPriv),
|
string(serializedMasterRootPriv),
|
||||||
)
|
)
|
||||||
zero.Bytes(serializedMasterRootPriv)
|
zero.Bytes(serializedMasterRootPriv)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
str := fmt.Sprintf("failed to create master extended " +
|
str := fmt.Sprintf("failed to create master extended " +
|
||||||
"private key")
|
"private key")
|
||||||
return nil, managerError(ErrKeyChain, str, err)
|
return nil, managerError(ErrKeyChain, str, err)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now that we have the root private key, we'll fetch the scope bucket
|
// Now that we have the root private key, we'll fetch the scope bucket
|
||||||
|
@ -591,21 +535,19 @@ func (m *Manager) NewScopedKeyManager(ns walletdb.ReadWriteBucket,
|
||||||
}
|
}
|
||||||
scopeKey := scopeToBytes(&scope)
|
scopeKey := scopeToBytes(&scope)
|
||||||
schemaBytes := scopeSchemaToBytes(&addrSchema)
|
schemaBytes := scopeSchemaToBytes(&addrSchema)
|
||||||
err := scopeSchemas.Put(scopeKey[:], schemaBytes)
|
err = scopeSchemas.Put(scopeKey[:], schemaBytes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !m.watchingOnly {
|
// With the database state created, we'll now derive the
|
||||||
// With the database state created, we'll now derive the
|
// cointype key using the master HD private key, then encrypt
|
||||||
// cointype key using the master HD private key, then encrypt
|
// it along with the first account using our crypto keys.
|
||||||
// it along with the first account using our crypto keys.
|
err = createManagerKeyScope(
|
||||||
err = createManagerKeyScope(
|
ns, scope, rootPriv, m.cryptoKeyPub, m.cryptoKeyPriv,
|
||||||
ns, scope, rootPriv, m.cryptoKeyPub, m.cryptoKeyPriv,
|
)
|
||||||
)
|
if err != nil {
|
||||||
if err != nil {
|
return nil, err
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Finally, we'll register this new scoped manager with the root
|
// Finally, we'll register this new scoped manager with the root
|
||||||
|
@ -886,19 +828,13 @@ func (m *Manager) ChainParams() *chaincfg.Params {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ChangePassphrase changes either the public or private passphrase to the
|
// ChangePassphrase changes either the public or private passphrase to the
|
||||||
// provided value depending on the private flag. In order to change the
|
// provided value depending on the private flag. The new passphrase keys are
|
||||||
// private password, the address manager must not be watching-only. The new
|
// derived using the scrypt parameters in the options, so changing the
|
||||||
// passphrase keys are derived using the scrypt parameters in the options, so
|
// passphrase may be used to bump the computational difficulty needed to brute
|
||||||
// changing the passphrase may be used to bump the computational difficulty
|
// force the passphrase.
|
||||||
// needed to brute force the passphrase.
|
|
||||||
func (m *Manager) ChangePassphrase(ns walletdb.ReadWriteBucket, oldPassphrase,
|
func (m *Manager) ChangePassphrase(ns walletdb.ReadWriteBucket, oldPassphrase,
|
||||||
newPassphrase []byte, private bool, config *ScryptOptions) error {
|
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()
|
m.mtx.Lock()
|
||||||
defer m.mtx.Unlock()
|
defer m.mtx.Unlock()
|
||||||
|
|
||||||
|
@ -1045,91 +981,6 @@ func (m *Manager) ChangePassphrase(ns walletdb.ReadWriteBucket, oldPassphrase,
|
||||||
return nil
|
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
|
// 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
|
// unlocked, the decryption key needed to decrypt private keys used for signing
|
||||||
// is in memory.
|
// 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
|
// Lock performs a best try effort to remove and zero all secret keys associated
|
||||||
// with the address manager.
|
// with the address manager.
|
||||||
//
|
|
||||||
// This function will return an error if invoked on a watching-only address
|
|
||||||
// manager.
|
|
||||||
func (m *Manager) Lock() error {
|
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()
|
m.mtx.Lock()
|
||||||
defer m.mtx.Unlock()
|
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
|
// 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,
|
// occur during this function will result in the address manager being locked,
|
||||||
// even if it was already unlocked prior to calling this function.
|
// 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 {
|
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()
|
m.mtx.Lock()
|
||||||
defer m.mtx.Unlock()
|
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) {
|
func (m *Manager) selectCryptoKey(keyType CryptoKeyType) (EncryptorDecryptor, error) {
|
||||||
if keyType == CKTPrivate || keyType == CKTScript {
|
if keyType == CKTPrivate || keyType == CKTScript {
|
||||||
// The manager must be unlocked to work with the private keys.
|
// 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)
|
return nil, managerError(ErrLocked, errLocked, nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1401,7 +1238,7 @@ func newManager(chainParams *chaincfg.Params, masterKeyPub *snacl.SecretKey,
|
||||||
masterKeyPriv *snacl.SecretKey, cryptoKeyPub EncryptorDecryptor,
|
masterKeyPriv *snacl.SecretKey, cryptoKeyPub EncryptorDecryptor,
|
||||||
cryptoKeyPrivEncrypted, cryptoKeyScriptEncrypted []byte, syncInfo *syncState,
|
cryptoKeyPrivEncrypted, cryptoKeyScriptEncrypted []byte, syncInfo *syncState,
|
||||||
birthday time.Time, privPassphraseSalt [saltSize]byte,
|
birthday time.Time, privPassphraseSalt [saltSize]byte,
|
||||||
scopedManagers map[KeyScope]*ScopedKeyManager, watchingOnly bool) *Manager {
|
scopedManagers map[KeyScope]*ScopedKeyManager) *Manager {
|
||||||
|
|
||||||
m := &Manager{
|
m := &Manager{
|
||||||
chainParams: chainParams,
|
chainParams: chainParams,
|
||||||
|
@ -1419,7 +1256,6 @@ func newManager(chainParams *chaincfg.Params, masterKeyPub *snacl.SecretKey,
|
||||||
scopedManagers: scopedManagers,
|
scopedManagers: scopedManagers,
|
||||||
externalAddrSchemas: make(map[AddressType][]KeyScope),
|
externalAddrSchemas: make(map[AddressType][]KeyScope),
|
||||||
internalAddrSchemas: make(map[AddressType][]KeyScope),
|
internalAddrSchemas: make(map[AddressType][]KeyScope),
|
||||||
watchingOnly: watchingOnly,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, sMgr := range m.scopedManagers {
|
for _, sMgr := range m.scopedManagers {
|
||||||
|
@ -1546,12 +1382,6 @@ func loadManager(ns walletdb.ReadBucket, pubPassphrase []byte,
|
||||||
return nil, managerError(ErrUpgrade, str, nil)
|
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.
|
// Load the master key params from the db.
|
||||||
masterKeyPubParams, masterKeyPrivParams, err := fetchMasterKeyParams(ns)
|
masterKeyPubParams, masterKeyPrivParams, err := fetchMasterKeyParams(ns)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -1579,15 +1409,13 @@ func loadManager(ns walletdb.ReadBucket, pubPassphrase []byte,
|
||||||
return nil, maybeConvertDbError(err)
|
return nil, maybeConvertDbError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// When not a watching-only manager, set the master private key params,
|
// Set the master private key params, but don't derive it now since the
|
||||||
// but don't derive it now since the manager starts off locked.
|
// manager starts off locked.
|
||||||
var masterKeyPriv snacl.SecretKey
|
var masterKeyPriv snacl.SecretKey
|
||||||
if !watchingOnly {
|
err = masterKeyPriv.Unmarshal(masterKeyPrivParams)
|
||||||
err := masterKeyPriv.Unmarshal(masterKeyPrivParams)
|
if err != nil {
|
||||||
if err != nil {
|
str := "failed to unmarshal master private key"
|
||||||
str := "failed to unmarshal master private key"
|
return nil, managerError(ErrCrypto, str, err)
|
||||||
return nil, managerError(ErrCrypto, str, err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Derive the master public key using the serialized params and provided
|
// Derive the master public key using the serialized params and provided
|
||||||
|
@ -1654,8 +1482,7 @@ func loadManager(ns walletdb.ReadBucket, pubPassphrase []byte,
|
||||||
mgr := newManager(
|
mgr := newManager(
|
||||||
chainParams, &masterKeyPub, &masterKeyPriv,
|
chainParams, &masterKeyPub, &masterKeyPriv,
|
||||||
cryptoKeyPub, cryptoKeyPrivEnc, cryptoKeyScriptEnc, syncInfo,
|
cryptoKeyPub, cryptoKeyPrivEnc, cryptoKeyScriptEnc, syncInfo,
|
||||||
birthday, privPassphraseSalt, scopedManagers, watchingOnly,
|
birthday, privPassphraseSalt, scopedManagers)
|
||||||
)
|
|
||||||
|
|
||||||
for _, scopedManager := range scopedManagers {
|
for _, scopedManager := range scopedManagers {
|
||||||
scopedManager.rootManager = mgr
|
scopedManager.rootManager = mgr
|
||||||
|
@ -1797,11 +1624,6 @@ func createManagerKeyScope(ns walletdb.ReadWriteBucket,
|
||||||
// derived. This allows all chained addresses in the address manager
|
// derived. This allows all chained addresses in the address manager
|
||||||
// to be recovered by using the same seed.
|
// 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
|
// All private and public keys and information are protected by secret
|
||||||
// keys derived from the provided private and public passphrases. The
|
// keys derived from the provided private and public passphrases. The
|
||||||
// public passphrase is required on subsequent opens of the address
|
// 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,
|
chainParams *chaincfg.Params, config *ScryptOptions,
|
||||||
birthday time.Time) error {
|
birthday time.Time) error {
|
||||||
|
|
||||||
// If the seed argument is nil we create in watchingOnly mode.
|
|
||||||
isWatchingOnly := rootKey == nil
|
|
||||||
|
|
||||||
// Return an error if the manager has already been created in
|
// Return an error if the manager has already been created in
|
||||||
// the given database namespace.
|
// the given database namespace.
|
||||||
exists := managerExists(ns)
|
exists := managerExists(ns)
|
||||||
|
@ -1831,17 +1650,13 @@ func Create(ns walletdb.ReadWriteBucket, rootKey *hdkeychain.ExtendedKey,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure the private passphrase is not empty.
|
// Ensure the private passphrase is not empty.
|
||||||
if !isWatchingOnly && len(privPassphrase) == 0 {
|
if len(privPassphrase) == 0 {
|
||||||
str := "private passphrase may not be empty"
|
str := "private passphrase may not be empty"
|
||||||
return managerError(ErrEmptyPassphrase, str, nil)
|
return managerError(ErrEmptyPassphrase, str, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Perform the initial bucket creation and database namespace setup.
|
// Perform the initial bucket creation and database namespace setup.
|
||||||
defaultScopes := map[KeyScope]ScopeAddrSchema{}
|
if err := createManagerNS(ns, ScopeAddrMap); err != nil {
|
||||||
if !isWatchingOnly {
|
|
||||||
defaultScopes = ScopeAddrMap
|
|
||||||
}
|
|
||||||
if err := createManagerNS(ns, defaultScopes); err != nil {
|
|
||||||
return maybeConvertDbError(err)
|
return maybeConvertDbError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1890,92 +1705,90 @@ func Create(ns walletdb.ReadWriteBucket, rootKey *hdkeychain.ExtendedKey,
|
||||||
var masterKeyPriv *snacl.SecretKey
|
var masterKeyPriv *snacl.SecretKey
|
||||||
var cryptoKeyPrivEnc []byte
|
var cryptoKeyPrivEnc []byte
|
||||||
var cryptoKeyScriptEnc []byte
|
var cryptoKeyScriptEnc []byte
|
||||||
if !isWatchingOnly {
|
masterKeyPriv, err = newSecretKey(&privPassphrase, config)
|
||||||
masterKeyPriv, err = newSecretKey(&privPassphrase, config)
|
if err != nil {
|
||||||
if err != nil {
|
str := "failed to master private key"
|
||||||
str := "failed to master private key"
|
return managerError(ErrCrypto, str, err)
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
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.
|
// Save the master key params to the database.
|
||||||
err = putMasterKeyParams(ns, pubParams, privParams)
|
err = putMasterKeyParams(ns, pubParams, privParams)
|
||||||
|
@ -1990,13 +1803,6 @@ func Create(ns walletdb.ReadWriteBucket, rootKey *hdkeychain.ExtendedKey,
|
||||||
return maybeConvertDbError(err)
|
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.
|
// Save the initial synced to state.
|
||||||
err = PutSyncedTo(ns, &syncInfo.syncedTo)
|
err = PutSyncedTo(ns, &syncInfo.syncedTo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -10,7 +10,6 @@ import (
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
@ -70,7 +69,6 @@ type testContext struct {
|
||||||
internalAccount uint32
|
internalAccount uint32
|
||||||
create bool
|
create bool
|
||||||
unlocked bool
|
unlocked bool
|
||||||
watchingOnly bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// addrType is the type of address being tested
|
// 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.
|
// for the expected error when the manager is locked.
|
||||||
gotPrivKey, err := gotAddr.PrivKey()
|
gotPrivKey, err := gotAddr.PrivKey()
|
||||||
switch {
|
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:
|
case tc.unlocked:
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tc.t.Errorf("%s PrivKey: unexpected error - got %v",
|
tc.t.Errorf("%s PrivKey: unexpected error - got %v",
|
||||||
|
@ -194,12 +186,6 @@ func testManagedPubKeyAddress(tc *testContext, prefix string,
|
||||||
// the manager is locked.
|
// the manager is locked.
|
||||||
gotWIF, err := gotAddr.ExportPrivKey()
|
gotWIF, err := gotAddr.ExportPrivKey()
|
||||||
switch {
|
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:
|
case tc.unlocked:
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tc.t.Errorf("%s ExportPrivKey: unexpected error - "+
|
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.
|
// the expected error when the manager is locked.
|
||||||
gotScript, err := gotAddr.Script()
|
gotScript, err := gotAddr.Script()
|
||||||
switch {
|
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
|
// Either the manger is unlocked or the script is not considered to
|
||||||
// be secret and is encrypted with the public key.
|
// be secret and is encrypted with the public key.
|
||||||
case tc.unlocked || wantAddr.scriptNotSecret:
|
case tc.unlocked || wantAddr.scriptNotSecret:
|
||||||
|
@ -448,13 +427,6 @@ func testExternalAddresses(tc *testContext) bool {
|
||||||
return false
|
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
|
// Unlock the manager and retest all of the addresses to ensure the
|
||||||
// private information is valid as well.
|
// private information is valid as well.
|
||||||
err := walletdb.View(tc.db, func(tx walletdb.ReadTx) error {
|
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
|
// retrieved by Address, and that they work properly when the manager is locked
|
||||||
// and unlocked.
|
// and unlocked.
|
||||||
func testInternalAddresses(tc *testContext) bool {
|
func testInternalAddresses(tc *testContext) bool {
|
||||||
// When the address manager is not in watching-only mode, unlocked it
|
// These tests reverse the order done in the external tests which starts
|
||||||
// first to ensure that address generation works correctly when the
|
// with a locked manager and unlock it afterwards.
|
||||||
// address manager is unlocked and then locked later. These tests
|
// Unlock the manager and retest all of the addresses to ensure the
|
||||||
// reverse the order done in the external tests which starts with a
|
// private information is valid as well.
|
||||||
// locked manager and unlock it afterwards.
|
err := walletdb.View(tc.db, func(tx walletdb.ReadTx) error {
|
||||||
if !tc.watchingOnly {
|
ns := tx.ReadBucket(waddrmgrNamespaceKey)
|
||||||
// Unlock the manager and retest all of the addresses to ensure the
|
return tc.rootManager.Unlock(ns, privPassphrase)
|
||||||
// private information is valid as well.
|
})
|
||||||
err := walletdb.View(tc.db, func(tx walletdb.ReadTx) error {
|
if err != nil {
|
||||||
ns := tx.ReadBucket(waddrmgrNamespaceKey)
|
tc.t.Errorf("Unlock: unexpected error: %v", err)
|
||||||
return tc.rootManager.Unlock(ns, privPassphrase)
|
return false
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
tc.t.Errorf("Unlock: unexpected error: %v", err)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
tc.unlocked = true
|
|
||||||
}
|
}
|
||||||
|
tc.unlocked = true
|
||||||
|
|
||||||
prefix := testNamePrefix(tc) + " testInternalAddresses"
|
prefix := testNamePrefix(tc) + " testInternalAddresses"
|
||||||
var addrs []ManagedAddress
|
var addrs []ManagedAddress
|
||||||
|
@ -597,24 +564,10 @@ func testInternalAddresses(tc *testContext) bool {
|
||||||
return true
|
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() {
|
if !testResults() {
|
||||||
return false
|
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
|
// Lock the manager and retest all of the addresses to ensure the
|
||||||
// public information remains valid and the private functions return
|
// public information remains valid and the private functions return
|
||||||
// the expected error.
|
// the expected error.
|
||||||
|
@ -639,55 +592,38 @@ func testLocking(tc *testContext) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Locking an already lock manager should return an error. The error
|
// Locking an already lock manager should return an error.
|
||||||
// should be ErrLocked or ErrWatchingOnly depending on the type of the
|
|
||||||
// address manager.
|
|
||||||
err := tc.rootManager.Lock()
|
err := tc.rootManager.Lock()
|
||||||
wantErrCode := ErrLocked
|
wantErrCode := ErrLocked
|
||||||
if tc.watchingOnly {
|
|
||||||
wantErrCode = ErrWatchingOnly
|
|
||||||
}
|
|
||||||
if !checkManagerError(tc.t, "Lock", err, wantErrCode) {
|
if !checkManagerError(tc.t, "Lock", err, wantErrCode) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure unlocking with the correct passphrase doesn't return any
|
// Ensure unlocking with the correct passphrase doesn't return any
|
||||||
// unexpected errors and the manager properly reports it is unlocked.
|
// 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 {
|
err = walletdb.View(tc.db, func(tx walletdb.ReadTx) error {
|
||||||
ns := tx.ReadBucket(waddrmgrNamespaceKey)
|
ns := tx.ReadBucket(waddrmgrNamespaceKey)
|
||||||
return tc.rootManager.Unlock(ns, privPassphrase)
|
return tc.rootManager.Unlock(ns, privPassphrase)
|
||||||
})
|
})
|
||||||
if tc.watchingOnly {
|
if err != nil {
|
||||||
if !checkManagerError(tc.t, "Unlock", err, ErrWatchingOnly) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
} else if err != nil {
|
|
||||||
tc.t.Errorf("Unlock: unexpected error: %v", err)
|
tc.t.Errorf("Unlock: unexpected error: %v", err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if !tc.watchingOnly && tc.rootManager.IsLocked() {
|
if tc.rootManager.IsLocked() {
|
||||||
tc.t.Error("IsLocked: returned true on unlocked manager")
|
tc.t.Error("IsLocked: returned true on unlocked manager")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unlocking the manager again is allowed. Since watching-only address
|
// Unlocking the manager again is allowed.
|
||||||
// managers can't be unlocked, also ensure the correct error for that
|
|
||||||
// case.
|
|
||||||
err = walletdb.View(tc.db, func(tx walletdb.ReadTx) error {
|
err = walletdb.View(tc.db, func(tx walletdb.ReadTx) error {
|
||||||
ns := tx.ReadBucket(waddrmgrNamespaceKey)
|
ns := tx.ReadBucket(waddrmgrNamespaceKey)
|
||||||
return tc.rootManager.Unlock(ns, privPassphrase)
|
return tc.rootManager.Unlock(ns, privPassphrase)
|
||||||
})
|
})
|
||||||
if tc.watchingOnly {
|
if err != nil {
|
||||||
if !checkManagerError(tc.t, "Unlock2", err, ErrWatchingOnly) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
} else if err != nil {
|
|
||||||
tc.t.Errorf("Unlock: unexpected error: %v", err)
|
tc.t.Errorf("Unlock: unexpected error: %v", err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if !tc.watchingOnly && tc.rootManager.IsLocked() {
|
if tc.rootManager.IsLocked() {
|
||||||
tc.t.Error("IsLocked: returned true on unlocked manager")
|
tc.t.Error("IsLocked: returned true on unlocked manager")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -699,9 +635,6 @@ func testLocking(tc *testContext) bool {
|
||||||
return tc.rootManager.Unlock(ns, []byte("invalidpassphrase"))
|
return tc.rootManager.Unlock(ns, []byte("invalidpassphrase"))
|
||||||
})
|
})
|
||||||
wantErrCode = ErrWrongPassphrase
|
wantErrCode = ErrWrongPassphrase
|
||||||
if tc.watchingOnly {
|
|
||||||
wantErrCode = ErrWatchingOnly
|
|
||||||
}
|
|
||||||
if !checkManagerError(tc.t, "Unlock", err, wantErrCode) {
|
if !checkManagerError(tc.t, "Unlock", err, wantErrCode) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -760,19 +693,16 @@ func testImportPrivateKey(tc *testContext) bool {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// The manager must be unlocked to import a private key, however a
|
// The manager must be unlocked to import a private key.
|
||||||
// watching-only manager can't be unlocked.
|
err := walletdb.View(tc.db, func(tx walletdb.ReadTx) error {
|
||||||
if !tc.watchingOnly {
|
ns := tx.ReadBucket(waddrmgrNamespaceKey)
|
||||||
err := walletdb.View(tc.db, func(tx walletdb.ReadTx) error {
|
return tc.rootManager.Unlock(ns, privPassphrase)
|
||||||
ns := tx.ReadBucket(waddrmgrNamespaceKey)
|
})
|
||||||
return tc.rootManager.Unlock(ns, privPassphrase)
|
if err != nil {
|
||||||
})
|
tc.t.Errorf("Unlock: unexpected error: %v", err)
|
||||||
if err != nil {
|
return false
|
||||||
tc.t.Errorf("Unlock: unexpected error: %v", err)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
tc.unlocked = true
|
|
||||||
}
|
}
|
||||||
|
tc.unlocked = true
|
||||||
|
|
||||||
// Only import the private keys when in the create phase of testing.
|
// Only import the private keys when in the create phase of testing.
|
||||||
tc.internalAccount = ImportedAddrAccount
|
tc.internalAccount = ImportedAddrAccount
|
||||||
|
@ -853,24 +783,10 @@ func testImportPrivateKey(tc *testContext) bool {
|
||||||
return !failed
|
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() {
|
if !testResults() {
|
||||||
return false
|
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
|
// Lock the manager and retest all of the addresses to ensure the
|
||||||
// private information returns the expected error.
|
// private information returns the expected error.
|
||||||
if err := tc.rootManager.Lock(); err != nil {
|
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
|
// The manager must be unlocked to import a private key and also for
|
||||||
// testing private data. However, a watching-only manager can't be
|
// testing private data.
|
||||||
// unlocked.
|
err := walletdb.View(tc.db, func(tx walletdb.ReadTx) error {
|
||||||
if !tc.watchingOnly {
|
ns := tx.ReadBucket(waddrmgrNamespaceKey)
|
||||||
err := walletdb.View(tc.db, func(tx walletdb.ReadTx) error {
|
return tc.rootManager.Unlock(ns, privPassphrase)
|
||||||
ns := tx.ReadBucket(waddrmgrNamespaceKey)
|
})
|
||||||
return tc.rootManager.Unlock(ns, privPassphrase)
|
if err != nil {
|
||||||
})
|
tc.t.Errorf("Unlock: unexpected error: %v", err)
|
||||||
if err != nil {
|
return false
|
||||||
tc.t.Errorf("Unlock: unexpected error: %v", err)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
tc.unlocked = true
|
|
||||||
}
|
}
|
||||||
|
tc.unlocked = true
|
||||||
|
|
||||||
// Only import the scripts when in the create phase of testing.
|
// Only import the scripts when in the create phase of testing.
|
||||||
tc.internalAccount = ImportedAddrAccount
|
tc.internalAccount = ImportedAddrAccount
|
||||||
|
@ -1110,24 +1023,10 @@ func testImportScript(tc *testContext) bool {
|
||||||
return !failed
|
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() {
|
if !testResults() {
|
||||||
return false
|
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
|
// Lock the manager and retest all of the addresses to ensure the
|
||||||
// private information returns the expected error.
|
// private information returns the expected error.
|
||||||
if err := tc.rootManager.Lock(); err != nil {
|
if err := tc.rootManager.Lock(); err != nil {
|
||||||
|
@ -1286,9 +1185,6 @@ func testChangePassphrase(tc *testContext) bool {
|
||||||
return false
|
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"
|
testName = pfx + "ChangePassphrase (private) with invalid old passphrase"
|
||||||
err = walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) error {
|
err = walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) error {
|
||||||
ns := tx.ReadWriteBucket(waddrmgrNamespaceKey)
|
ns := tx.ReadWriteBucket(waddrmgrNamespaceKey)
|
||||||
|
@ -1297,21 +1193,10 @@ func testChangePassphrase(tc *testContext) bool {
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
wantErrCode := ErrWrongPassphrase
|
wantErrCode := ErrWrongPassphrase
|
||||||
if tc.watchingOnly {
|
|
||||||
wantErrCode = ErrWatchingOnly
|
|
||||||
}
|
|
||||||
if !checkManagerError(tc.t, testName, err, wantErrCode) {
|
if !checkManagerError(tc.t, testName, err, wantErrCode) {
|
||||||
return false
|
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.
|
// Change the private passphrase.
|
||||||
testName = pfx + "ChangePassphrase (private)"
|
testName = pfx + "ChangePassphrase (private)"
|
||||||
err = walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) error {
|
err = walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) error {
|
||||||
|
@ -1368,22 +1253,6 @@ func testChangePassphrase(tc *testContext) bool {
|
||||||
// testNewAccount tests the new account creation func of the address manager works
|
// testNewAccount tests the new account creation func of the address manager works
|
||||||
// as expected.
|
// as expected.
|
||||||
func testNewAccount(tc *testContext) bool {
|
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
|
// Creating new accounts when wallet is locked should return ErrLocked
|
||||||
err := walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) error {
|
err := walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) error {
|
||||||
ns := tx.ReadWriteBucket(waddrmgrNamespaceKey)
|
ns := tx.ReadWriteBucket(waddrmgrNamespaceKey)
|
||||||
|
@ -1711,148 +1580,26 @@ func testForEachAccountAddress(tc *testContext) bool {
|
||||||
// testManagerAPI tests the functions provided by the Manager API as well as
|
// testManagerAPI tests the functions provided by the Manager API as well as
|
||||||
// the ManagedAddress, ManagedPubKeyAddress, and ManagedScriptAddress
|
// the ManagedAddress, ManagedPubKeyAddress, and ManagedScriptAddress
|
||||||
// interfaces.
|
// interfaces.
|
||||||
func testManagerAPI(tc *testContext, caseCreatedWatchingOnly bool) {
|
func testManagerAPI(tc *testContext) {
|
||||||
if !caseCreatedWatchingOnly {
|
// Test API for normal create (w/ seed) case.
|
||||||
// Test API for normal create (w/ seed) case.
|
testLocking(tc)
|
||||||
testLocking(tc)
|
testExternalAddresses(tc)
|
||||||
testExternalAddresses(tc)
|
testInternalAddresses(tc)
|
||||||
testInternalAddresses(tc)
|
testImportPrivateKey(tc)
|
||||||
testImportPrivateKey(tc)
|
testImportScript(tc)
|
||||||
testImportScript(tc)
|
testMarkUsed(tc, true)
|
||||||
testMarkUsed(tc, true)
|
testChangePassphrase(tc)
|
||||||
testChangePassphrase(tc)
|
|
||||||
|
|
||||||
// Reset default account
|
// Reset default account
|
||||||
tc.internalAccount = 0
|
tc.internalAccount = 0
|
||||||
testNewAccount(tc)
|
testNewAccount(tc)
|
||||||
testLookupAccount(tc)
|
testLookupAccount(tc)
|
||||||
testForEachAccount(tc)
|
testForEachAccount(tc)
|
||||||
testForEachAccountAddress(tc)
|
testForEachAccountAddress(tc)
|
||||||
|
|
||||||
// Rename account 1 "acct-create"
|
// Rename account 1 "acct-create"
|
||||||
tc.internalAccount = 1
|
tc.internalAccount = 1
|
||||||
testRenameAccount(tc)
|
testRenameAccount(tc)
|
||||||
} else {
|
|
||||||
// Test API for created watch-only case.
|
|
||||||
testExternalAddresses(tc)
|
|
||||||
testInternalAddresses(tc)
|
|
||||||
testMarkUsed(tc, false)
|
|
||||||
testChangePassphrase(tc)
|
|
||||||
|
|
||||||
testNewAccount(tc)
|
|
||||||
expectedAccounts := map[string]uint32{
|
|
||||||
defaultAccountName: DefaultAccountNum,
|
|
||||||
}
|
|
||||||
testLookupExpectedAccount(tc, expectedAccounts, 0)
|
|
||||||
//testForEachAccount(tc)
|
|
||||||
testForEachAccountAddress(tc)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// testSync tests various facets of setting the manager sync state.
|
// 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.
|
// much of the testing involves having specific state.
|
||||||
func _TestManager(t *testing.T) {
|
func _TestManager(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
createdWatchingOnly bool
|
rootKey *hdkeychain.ExtendedKey
|
||||||
rootKey *hdkeychain.ExtendedKey
|
privPassphrase []byte
|
||||||
privPassphrase []byte
|
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "created with seed",
|
name: "created with seed",
|
||||||
createdWatchingOnly: false,
|
rootKey: rootKey,
|
||||||
rootKey: rootKey,
|
privPassphrase: privPassphrase,
|
||||||
privPassphrase: privPassphrase,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "created watch-only",
|
|
||||||
createdWatchingOnly: true,
|
|
||||||
rootKey: nil,
|
|
||||||
privPassphrase: nil,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
// Need to wrap in a call so the defers work correctly.
|
// Need to wrap in a call so the defers work correctly.
|
||||||
testManagerCase(
|
testManagerCase(t, test.name, test.privPassphrase, test.rootKey)
|
||||||
t, test.name, test.createdWatchingOnly,
|
|
||||||
test.privPassphrase, test.rootKey,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func testManagerCase(t *testing.T, caseName string,
|
func testManagerCase(t *testing.T, caseName string,
|
||||||
caseCreatedWatchingOnly bool, casePrivPassphrase []byte,
|
casePrivPassphrase []byte, caseKey *hdkeychain.ExtendedKey) {
|
||||||
caseKey *hdkeychain.ExtendedKey) {
|
|
||||||
|
|
||||||
teardown, db := emptyDB(t)
|
teardown, db := emptyDB(t)
|
||||||
defer teardown()
|
defer teardown()
|
||||||
|
|
||||||
if !caseCreatedWatchingOnly {
|
// Open manager that does not exist to ensure the expected error is
|
||||||
// Open manager that does not exist to ensure the expected error is
|
// returned.
|
||||||
// returned.
|
err := walletdb.View(db, func(tx walletdb.ReadTx) error {
|
||||||
err := walletdb.View(db, func(tx walletdb.ReadTx) error {
|
ns := tx.ReadBucket(waddrmgrNamespaceKey)
|
||||||
ns := tx.ReadBucket(waddrmgrNamespaceKey)
|
_, err := Open(ns, pubPassphrase, &chaincfg.MainNetParams)
|
||||||
_, err := Open(ns, pubPassphrase, &chaincfg.MainNetParams)
|
return err
|
||||||
return err
|
})
|
||||||
})
|
if !checkManagerError(t, "Open non-existent", err, ErrNoExist) {
|
||||||
if !checkManagerError(t, "Open non-existent", err, ErrNoExist) {
|
return
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a new manager.
|
// Create a new manager.
|
||||||
var mgr *Manager
|
var mgr *Manager
|
||||||
err := walletdb.Update(db, func(tx walletdb.ReadWriteTx) error {
|
err = walletdb.Update(db, func(tx walletdb.ReadWriteTx) error {
|
||||||
ns, err := tx.CreateTopLevelBucket(waddrmgrNamespaceKey)
|
ns, err := tx.CreateTopLevelBucket(waddrmgrNamespaceKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -1981,10 +1714,6 @@ func testManagerCase(t *testing.T, caseName string,
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if caseCreatedWatchingOnly {
|
|
||||||
_, err = mgr.NewScopedKeyManager(
|
|
||||||
ns, KeyScopeBIP0044, ScopeAddrMap[KeyScopeBIP0044])
|
|
||||||
}
|
|
||||||
return err
|
return err
|
||||||
})
|
})
|
||||||
if err != nil {
|
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)
|
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
|
// Run all of the manager API tests in create mode and close the
|
||||||
// manager after they've completed
|
// manager after they've completed
|
||||||
testManagerAPI(&testContext{
|
testManagerAPI(&testContext{
|
||||||
|
@ -2051,8 +1754,7 @@ func testManagerCase(t *testing.T, caseName string,
|
||||||
rootManager: mgr,
|
rootManager: mgr,
|
||||||
internalAccount: 0,
|
internalAccount: 0,
|
||||||
create: true,
|
create: true,
|
||||||
watchingOnly: caseCreatedWatchingOnly,
|
})
|
||||||
}, caseCreatedWatchingOnly)
|
|
||||||
mgr.Close()
|
mgr.Close()
|
||||||
|
|
||||||
// Open the manager and run all the tests again in open mode which
|
// Open the manager and run all the tests again in open mode which
|
||||||
|
@ -2081,29 +1783,20 @@ func testManagerCase(t *testing.T, caseName string,
|
||||||
rootManager: mgr,
|
rootManager: mgr,
|
||||||
internalAccount: 0,
|
internalAccount: 0,
|
||||||
create: false,
|
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.
|
// Ensure that the manager sync state functionality works as expected.
|
||||||
testSync(tc)
|
testSync(tc)
|
||||||
|
|
||||||
if !caseCreatedWatchingOnly {
|
// Unlock the manager so it can be closed with it unlocked to ensure
|
||||||
// Unlock the manager so it can be closed with it unlocked to ensure
|
// it works without issue.
|
||||||
// it works without issue.
|
err = walletdb.View(db, func(tx walletdb.ReadTx) error {
|
||||||
err = walletdb.View(db, func(tx walletdb.ReadTx) error {
|
ns := tx.ReadBucket(waddrmgrNamespaceKey)
|
||||||
ns := tx.ReadBucket(waddrmgrNamespaceKey)
|
return mgr.Unlock(ns, casePrivPassphrase)
|
||||||
return mgr.Unlock(ns, casePrivPassphrase)
|
})
|
||||||
})
|
if err != nil {
|
||||||
if err != nil {
|
t.Errorf("Unlock: unexpected error: %v", err)
|
||||||
t.Errorf("Unlock: unexpected error: %v", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2683,142 +2376,6 @@ func TestNewRawAccount(t *testing.T) {
|
||||||
testNewRawAccount(t, mgr, db, accountNum, scopedMgr)
|
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,
|
func testNewRawAccount(t *testing.T, _ *Manager, db walletdb.DB,
|
||||||
accountNum uint32, scopedMgr *ScopedKeyManager) {
|
accountNum uint32, scopedMgr *ScopedKeyManager) {
|
||||||
// With the account created, we should be able to derive new addresses
|
// With the account created, we should be able to derive new addresses
|
||||||
|
|
|
@ -393,10 +393,7 @@ func (s *ScopedKeyManager) loadAccountInfo(ns walletdb.ReadBucket,
|
||||||
return hdkeychain.NewKeyFromString(string(serializedKey))
|
return hdkeychain.NewKeyFromString(string(serializedKey))
|
||||||
}
|
}
|
||||||
|
|
||||||
// The wallet will only contain private keys for default accounts if the
|
hasPrivateKey := !s.rootManager.isLocked()
|
||||||
// wallet's not set up as watch-only and it's been unlocked.
|
|
||||||
watchOnly := s.rootManager.watchOnly()
|
|
||||||
hasPrivateKey := !s.rootManager.isLocked() && !watchOnly
|
|
||||||
|
|
||||||
// Create the new account info with the known information. The rest of
|
// Create the new account info with the known information. The rest of
|
||||||
// the fields are filled out below.
|
// 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:
|
default:
|
||||||
str := fmt.Sprintf("unsupported account type %T", row)
|
str := fmt.Sprintf("unsupported account type %T", row)
|
||||||
return nil, managerError(ErrDatabase, str, nil)
|
return nil, managerError(ErrDatabase, str, nil)
|
||||||
|
@ -546,8 +520,6 @@ func (s *ScopedKeyManager) AccountProperties(ns walletdb.ReadBucket,
|
||||||
props.InternalKeyCount = acctInfo.nextInternalIndex
|
props.InternalKeyCount = acctInfo.nextInternalIndex
|
||||||
props.AccountPubKey = acctInfo.acctKeyPub
|
props.AccountPubKey = acctInfo.acctKeyPub
|
||||||
props.MasterKeyFingerprint = acctInfo.masterKeyFingerprint
|
props.MasterKeyFingerprint = acctInfo.masterKeyFingerprint
|
||||||
props.IsWatchOnly = s.rootManager.WatchOnly() ||
|
|
||||||
acctInfo.acctKeyPriv == nil
|
|
||||||
props.AddrSchema = acctInfo.addrSchema
|
props.AddrSchema = acctInfo.addrSchema
|
||||||
|
|
||||||
// Export the account public key with the correct version
|
// Export the account public key with the correct version
|
||||||
|
@ -574,7 +546,6 @@ func (s *ScopedKeyManager) AccountProperties(ns walletdb.ReadBucket,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
props.AccountName = ImportedAddrAccountName // reserved, nonchangable
|
props.AccountName = ImportedAddrAccountName // reserved, nonchangable
|
||||||
props.IsWatchOnly = s.rootManager.WatchOnly()
|
|
||||||
|
|
||||||
// Could be more efficient if this was tracked by the db.
|
// Could be more efficient if this was tracked by the db.
|
||||||
var importedKeyCount uint32
|
var importedKeyCount uint32
|
||||||
|
@ -642,8 +613,7 @@ func (s *ScopedKeyManager) DeriveFromKeyPathCache(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
watchOnly := s.rootManager.WatchOnly()
|
private := !s.rootManager.IsLocked()
|
||||||
private := !s.rootManager.IsLocked() && !watchOnly
|
|
||||||
|
|
||||||
// Now that we have the account information, we can derive the key
|
// Now that we have the account information, we can derive the key
|
||||||
// directly.
|
// directly.
|
||||||
|
@ -684,8 +654,7 @@ func (s *ScopedKeyManager) DeriveFromKeyPath(ns walletdb.ReadBucket,
|
||||||
s.mtx.Lock()
|
s.mtx.Lock()
|
||||||
defer s.mtx.Unlock()
|
defer s.mtx.Unlock()
|
||||||
|
|
||||||
watchOnly := s.rootManager.WatchOnly()
|
private := !s.rootManager.IsLocked()
|
||||||
private := !s.rootManager.IsLocked() && !watchOnly
|
|
||||||
|
|
||||||
addrKey, _, _, err := s.deriveKeyFromPath(
|
addrKey, _, _, err := s.deriveKeyFromPath(
|
||||||
ns, kp.InternalAccount, kp.Branch, kp.Index, private,
|
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
|
// Since the manger's mutex is assumed to held when invoking this
|
||||||
// function, we use the internal isLocked to avoid a deadlock.
|
// 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(
|
addressKey, acctKey, masterKeyFingerprint, err := s.deriveKeyFromPath(
|
||||||
ns, row.account, row.branch, row.index, private,
|
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
|
// Choose the account key to used based on whether the address manager
|
||||||
// is locked.
|
// is locked.
|
||||||
acctKey := acctInfo.acctKeyPub
|
acctKey := acctInfo.acctKeyPub
|
||||||
watchOnly := s.rootManager.WatchOnly() || len(acctInfo.acctKeyEncrypted) == 0
|
if !s.rootManager.IsLocked() {
|
||||||
if !s.rootManager.IsLocked() && !watchOnly {
|
|
||||||
acctKey = acctInfo.acctKeyPriv
|
acctKey = acctInfo.acctKeyPriv
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1138,7 +1106,7 @@ func (s *ScopedKeyManager) nextAddresses(ns walletdb.ReadWriteBucket,
|
||||||
// Add the new managed address to the list of addresses
|
// Add the new managed address to the list of addresses
|
||||||
// that need their private keys derived when the
|
// that need their private keys derived when the
|
||||||
// address manager is next unlocked.
|
// address manager is next unlocked.
|
||||||
if s.rootManager.isLocked() && !watchOnly {
|
if s.rootManager.isLocked() {
|
||||||
s.deriveOnUnlock = append(s.deriveOnUnlock, info)
|
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
|
// Choose the account key to used based on whether the address manager
|
||||||
// is locked.
|
// is locked.
|
||||||
acctKey := acctInfo.acctKeyPub
|
acctKey := acctInfo.acctKeyPub
|
||||||
watchOnly := s.rootManager.WatchOnly() || acctInfo.acctKeyPriv != nil
|
if !s.rootManager.IsLocked() {
|
||||||
if !s.rootManager.IsLocked() && !watchOnly {
|
|
||||||
acctKey = acctInfo.acctKeyPriv
|
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
|
// Add the new managed address to the list of addresses that
|
||||||
// need their private keys derived when the address manager is
|
// need their private keys derived when the address manager is
|
||||||
// next unlocked.
|
// next unlocked.
|
||||||
if s.rootManager.IsLocked() && !watchOnly {
|
if s.rootManager.IsLocked() {
|
||||||
s.deriveOnUnlock = append(s.deriveOnUnlock, info)
|
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
|
// number *directly*, rather than taking a string name for the account, then
|
||||||
// mapping that to the next highest account number.
|
// mapping that to the next highest account number.
|
||||||
func (s *ScopedKeyManager) NewRawAccount(ns walletdb.ReadWriteBucket, number uint32) error {
|
func (s *ScopedKeyManager) NewRawAccount(ns walletdb.ReadWriteBucket, number uint32) error {
|
||||||
if s.rootManager.WatchOnly() {
|
|
||||||
return managerError(ErrWatchingOnly, errWatchingOnly, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
s.mtx.Lock()
|
s.mtx.Lock()
|
||||||
defer s.mtx.Unlock()
|
defer s.mtx.Unlock()
|
||||||
|
@ -1507,45 +1471,12 @@ func (s *ScopedKeyManager) NewRawAccount(ns walletdb.ReadWriteBucket, number uin
|
||||||
return s.newAccount(ns, number, name)
|
return s.newAccount(ns, number, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewRawAccountWatchingOnly creates a new watching only account for the scoped
|
|
||||||
// manager. This method differs from the NewAccountWatchingOnly method in that
|
|
||||||
// this method takes the account number *directly*, rather than taking a string
|
|
||||||
// name for the account, then mapping that to the next highest account number.
|
|
||||||
//
|
|
||||||
// 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
|
// NewAccount creates and returns a new account stored in the manager based on
|
||||||
// the given account name. If an account with the same name already exists,
|
// the given account name. If an account with the same name already exists,
|
||||||
// ErrDuplicateAccount will be returned. Since creating a new account requires
|
// ErrDuplicateAccount will be returned. Since creating a new account requires
|
||||||
// access to the cointype keys (from which extended account keys are derived),
|
// access to the cointype keys (from which extended account keys are derived),
|
||||||
// it requires the manager to be unlocked.
|
// it requires the manager to be unlocked.
|
||||||
func (s *ScopedKeyManager) NewAccount(ns walletdb.ReadWriteBucket, name string) (uint32, error) {
|
func (s *ScopedKeyManager) NewAccount(ns walletdb.ReadWriteBucket, name string) (uint32, error) {
|
||||||
if s.rootManager.WatchOnly() {
|
|
||||||
return 0, managerError(ErrWatchingOnly, errWatchingOnly, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
s.mtx.Lock()
|
s.mtx.Lock()
|
||||||
defer s.mtx.Unlock()
|
defer s.mtx.Unlock()
|
||||||
|
@ -1654,95 +1585,6 @@ func (s *ScopedKeyManager) newAccount(ns walletdb.ReadWriteBucket,
|
||||||
return putLastAccount(ns, &s.scope, account)
|
return putLastAccount(ns, &s.scope, account)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewAccountWatchingOnly is similar to NewAccount, but for watch-only wallets.
|
|
||||||
//
|
|
||||||
// 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
|
// RenameAccount renames an account stored in the manager based on the given
|
||||||
// account number with the given name. If an account with the same name
|
// account number with the given name. If an account with the same name
|
||||||
// already exists, ErrDuplicateAccount will be returned.
|
// already exists, ErrDuplicateAccount will be returned.
|
||||||
|
@ -1796,21 +1638,6 @@ func (s *ScopedKeyManager) RenameAccount(ns walletdb.ReadWriteBucket,
|
||||||
return err
|
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:
|
default:
|
||||||
str := fmt.Sprintf("unsupported account type %T", row)
|
str := fmt.Sprintf("unsupported account type %T", row)
|
||||||
return managerError(ErrDatabase, str, nil)
|
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
|
// All imported addresses will be part of the account defined by the
|
||||||
// ImportedAddrAccount constant.
|
// ImportedAddrAccount constant.
|
||||||
//
|
//
|
||||||
// NOTE: When the address manager is watching-only, the private key itself will
|
// This function will return an error if the address manager is locked, or
|
||||||
// not be stored or available since it is private data. Instead, only the
|
// not for the same network as the key trying to be imported.
|
||||||
// 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.
|
|
||||||
// It will also return an error if the address already exists. Any other
|
// It will also return an error if the address already exists. Any other
|
||||||
// errors returned are generally unexpected.
|
// errors returned are generally unexpected.
|
||||||
func (s *ScopedKeyManager) ImportPrivateKey(ns walletdb.ReadWriteBucket,
|
func (s *ScopedKeyManager) ImportPrivateKey(ns walletdb.ReadWriteBucket,
|
||||||
|
@ -1860,25 +1681,23 @@ func (s *ScopedKeyManager) ImportPrivateKey(ns walletdb.ReadWriteBucket,
|
||||||
defer s.mtx.Unlock()
|
defer s.mtx.Unlock()
|
||||||
|
|
||||||
// The manager must be unlocked to encrypt the imported private key.
|
// 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)
|
return nil, managerError(ErrLocked, errLocked, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Encrypt the private key when not a watching-only address manager.
|
// Encrypt the private key.
|
||||||
var encryptedPrivKey []byte
|
var encryptedPrivKey []byte
|
||||||
if !s.rootManager.WatchOnly() {
|
privKeyBytes := wif.PrivKey.Serialize()
|
||||||
privKeyBytes := wif.PrivKey.Serialize()
|
var err error
|
||||||
var err error
|
encryptedPrivKey, err = s.rootManager.cryptoKeyPriv.Encrypt(privKeyBytes)
|
||||||
encryptedPrivKey, err = s.rootManager.cryptoKeyPriv.Encrypt(privKeyBytes)
|
zero.Bytes(privKeyBytes)
|
||||||
zero.Bytes(privKeyBytes)
|
if err != nil {
|
||||||
if err != nil {
|
str := fmt.Sprintf("failed to encrypt private key for %x",
|
||||||
str := fmt.Sprintf("failed to encrypt private key for %x",
|
wif.PrivKey.PubKey().SerializeCompressed())
|
||||||
wif.PrivKey.PubKey().SerializeCompressed())
|
return nil, managerError(ErrCrypto, str, err)
|
||||||
return nil, managerError(ErrCrypto, str, err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err := s.importPublicKey(
|
err = s.importPublicKey(
|
||||||
ns, wif.SerializePubKey(), encryptedPrivKey,
|
ns, wif.SerializePubKey(), encryptedPrivKey,
|
||||||
s.addrSchema.ExternalAddrType, bs,
|
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.
|
// Create a new managed address based on the imported address.
|
||||||
if !s.rootManager.WatchOnly() {
|
return s.toImportedPrivateManagedAddress(wif)
|
||||||
return s.toImportedPrivateManagedAddress(wif)
|
|
||||||
}
|
|
||||||
pubKey := (*btcec.PublicKey)(&wif.PrivKey.PublicKey)
|
|
||||||
return s.toImportedPublicManagedAddress(pubKey, wif.CompressPubKey)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ImportPublicKey imports a public key into the address manager.
|
// 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
|
// All imported script addresses will be part of the account defined by the
|
||||||
// ImportedAddrAccount constant.
|
// ImportedAddrAccount constant.
|
||||||
//
|
//
|
||||||
// When the address manager is watching-only, the script itself will not be
|
// This function will return an error if the address manager is locked, or the
|
||||||
// stored or available since it is considered private data.
|
// address already exists. Any other errors returned are generally unexpected.
|
||||||
//
|
|
||||||
// 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.
|
|
||||||
func (s *ScopedKeyManager) ImportScript(ns walletdb.ReadWriteBucket,
|
func (s *ScopedKeyManager) ImportScript(ns walletdb.ReadWriteBucket,
|
||||||
script []byte, bs *BlockStamp) (ManagedScriptAddress, error) {
|
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
|
// All imported script addresses will be part of the account defined by the
|
||||||
// ImportedAddrAccount constant.
|
// ImportedAddrAccount constant.
|
||||||
//
|
//
|
||||||
// When the address manager is watching-only, the script itself will not be
|
// This function will return an error if the address manager is locked, or the
|
||||||
// stored or available since it is considered private data.
|
// address already exists. Any other errors returned are generally unexpected.
|
||||||
//
|
|
||||||
// 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.
|
|
||||||
func (s *ScopedKeyManager) ImportWitnessScript(ns walletdb.ReadWriteBucket,
|
func (s *ScopedKeyManager) ImportWitnessScript(ns walletdb.ReadWriteBucket,
|
||||||
script []byte, bs *BlockStamp, witnessVersion byte,
|
script []byte, bs *BlockStamp, witnessVersion byte,
|
||||||
isSecretScript bool) (ManagedScriptAddress, error) {
|
isSecretScript bool) (ManagedScriptAddress, error) {
|
||||||
|
@ -2099,13 +1906,6 @@ func (s *ScopedKeyManager) importScriptAddress(ns walletdb.ReadWriteBucket,
|
||||||
return nil, managerError(ErrLocked, errLocked, nil)
|
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.
|
// Witness script addresses use a SHA256.
|
||||||
var scriptHash []byte
|
var scriptHash []byte
|
||||||
switch addrType {
|
switch addrType {
|
||||||
|
@ -2124,8 +1924,8 @@ func (s *ScopedKeyManager) importScriptAddress(ns walletdb.ReadWriteBucket,
|
||||||
return nil, managerError(ErrDuplicateAddress, str, nil)
|
return nil, managerError(ErrDuplicateAddress, str, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Encrypt the script hash using the crypto public key so it is
|
// Encrypt the script hash using the crypto public key so it not
|
||||||
// accessible when the address manager is locked or watching-only.
|
// accessible when the address manager is locked.
|
||||||
encryptedHash, err := s.rootManager.cryptoKeyPub.Encrypt(scriptHash)
|
encryptedHash, err := s.rootManager.cryptoKeyPub.Encrypt(scriptHash)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
str := fmt.Sprintf("failed to encrypt script hash %x",
|
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,
|
// Create a new managed address based on the imported script. Also,
|
||||||
// when not a watching-only address manager, make a copy of the script
|
// make a copy of the script since it will be cleared on lock and the
|
||||||
// since it will be cleared on lock and the script the caller passed
|
// script the caller passed // should not be cleared out from under the
|
||||||
// should not be cleared out from under the caller.
|
// caller.
|
||||||
var (
|
var (
|
||||||
managedAddr ManagedScriptAddress
|
managedAddr ManagedScriptAddress
|
||||||
baseScriptAddr *baseScriptAddress
|
baseScriptAddr *baseScriptAddress
|
||||||
|
@ -2390,22 +2190,6 @@ func (s *ScopedKeyManager) ForEachInternalActiveAddress(ns walletdb.ReadBucket,
|
||||||
return nil
|
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
|
// 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
|
// 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
|
// accounts as they are stored within the database using the legacy BIP-0044
|
||||||
|
|
|
@ -197,40 +197,18 @@ func (w *Wallet) txToOutputs(outputs []*wire.TxOut, keyScope *waddrmgr.KeyScope,
|
||||||
return walletdb.ErrDryRunRollBack
|
return walletdb.ErrDryRunRollBack
|
||||||
}
|
}
|
||||||
|
|
||||||
// Before committing the transaction, we'll sign our inputs. If
|
err = tx.AddAllInputScripts(
|
||||||
// the inputs are part of a watch-only account, there's no
|
secretSource{w.Manager, addrmgrNs},
|
||||||
// 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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if !watchOnly {
|
|
||||||
err = tx.AddAllInputScripts(
|
|
||||||
secretSource{w.Manager, addrmgrNs},
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = validateMsgTx(
|
err = validateMsgTx(
|
||||||
tx.Tx, tx.PrevScripts, tx.PrevInputValues,
|
tx.Tx, tx.PrevScripts, tx.PrevInputValues,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if tx.ChangeIndex >= 0 && account == waddrmgr.ImportedAddrAccount {
|
if tx.ChangeIndex >= 0 && account == waddrmgr.ImportedAddrAccount {
|
||||||
|
|
|
@ -8,8 +8,6 @@ import (
|
||||||
|
|
||||||
"github.com/lbryio/lbcd/chaincfg"
|
"github.com/lbryio/lbcd/chaincfg"
|
||||||
"github.com/lbryio/lbcutil/hdkeychain"
|
"github.com/lbryio/lbcutil/hdkeychain"
|
||||||
"github.com/lbryio/lbcwallet/waddrmgr"
|
|
||||||
"github.com/lbryio/lbcwallet/walletdb"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// defaultDBTimeout specifies the timeout value when opening the wallet
|
// defaultDBTimeout specifies the timeout value when opening the wallet
|
||||||
|
@ -53,48 +51,3 @@ func testWallet(t *testing.T) (*Wallet, func()) {
|
||||||
|
|
||||||
return w, cleanup
|
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
|
|
||||||
}
|
|
||||||
|
|
|
@ -268,12 +268,14 @@ func (w *Wallet) importAccountScope(ns walletdb.ReadWriteBucket, name string,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
account, err := scopedMgr.NewAccountWatchingOnly(
|
// FIXME
|
||||||
ns, name, accountPubKey, masterKeyFingerprint, addrSchema,
|
// account, err := scopedMgr.NewAccountWatchingOnly(
|
||||||
)
|
// ns, name, accountPubKey, masterKeyFingerprint, addrSchema,
|
||||||
if err != nil {
|
// )
|
||||||
return nil, err
|
// if err != nil {
|
||||||
}
|
// return nil, err
|
||||||
|
// }
|
||||||
|
var account uint32
|
||||||
return scopedMgr.AccountProperties(ns, account)
|
return scopedMgr.AccountProperties(ns, account)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -137,23 +137,12 @@ func _TestImportAccount(t *testing.T) {
|
||||||
w, cleanup := testWallet(t)
|
w, cleanup := testWallet(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
testImportAccount(t, w, tc, false, tc.name)
|
testImportAccount(t, w, tc, 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)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func testImportAccount(t *testing.T, w *Wallet, tc *testCase, watchOnly bool,
|
func testImportAccount(t *testing.T, w *Wallet, tc *testCase, name string) {
|
||||||
name string) {
|
|
||||||
|
|
||||||
// First derive the master public key of the account we want to import.
|
// First derive the master public key of the account we want to import.
|
||||||
root, err := hdkeychain.NewKeyFromString(tc.masterPriv)
|
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
|
// If the wallet is watch only, there is no default account and our
|
||||||
// imported account will be index 0.
|
// imported account will be index 0.
|
||||||
firstAccountIndex := uint32(1)
|
firstAccountIndex := uint32(1)
|
||||||
if watchOnly {
|
|
||||||
firstAccountIndex = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// We should have 3 additional accounts now.
|
// We should have 3 additional accounts now.
|
||||||
acctResult, err := w.Accounts(tc.expectedScope)
|
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.
|
// Validate the state of the accounts.
|
||||||
require.Equal(t, firstAccountIndex, acct1.AccountNumber)
|
require.Equal(t, firstAccountIndex, acct1.AccountNumber)
|
||||||
require.Equal(t, name+"1", acct1.AccountName)
|
require.Equal(t, name+"1", acct1.AccountName)
|
||||||
require.Equal(t, true, acct1.IsWatchOnly)
|
|
||||||
require.Equal(t, root.ParentFingerprint(), acct1.MasterKeyFingerprint)
|
require.Equal(t, root.ParentFingerprint(), acct1.MasterKeyFingerprint)
|
||||||
require.NotNil(t, acct1.AccountPubKey)
|
require.NotNil(t, acct1.AccountPubKey)
|
||||||
require.Equal(t, acct1Pub.String(), acct1.AccountPubKey.String())
|
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, firstAccountIndex+1, acct2.AccountNumber)
|
||||||
require.Equal(t, name+"2", acct2.AccountName)
|
require.Equal(t, name+"2", acct2.AccountName)
|
||||||
require.Equal(t, true, acct2.IsWatchOnly)
|
|
||||||
require.Equal(t, root.ParentFingerprint(), acct2.MasterKeyFingerprint)
|
require.Equal(t, root.ParentFingerprint(), acct2.MasterKeyFingerprint)
|
||||||
require.NotNil(t, acct2.AccountPubKey)
|
require.NotNil(t, acct2.AccountPubKey)
|
||||||
require.Equal(t, acct2Pub.String(), acct2.AccountPubKey.String())
|
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.
|
// Make sure we can't get private keys for the imported accounts.
|
||||||
_, err = w.DumpWIFPrivateKey(intAddr)
|
_, err = w.DumpWIFPrivateKey(intAddr)
|
||||||
require.True(t, waddrmgr.IsError(err, waddrmgr.ErrWatchingOnly))
|
|
||||||
|
|
||||||
// Get the address info for the single key we imported.
|
// Get the address info for the single key we imported.
|
||||||
switch tc.addrType {
|
switch tc.addrType {
|
||||||
|
|
|
@ -168,9 +168,7 @@ func (l *Loader) CreateNewWallet(pubPassphrase, privPassphrase, seed []byte,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return l.createNewWallet(
|
return l.createNewWallet(pubPassphrase, privPassphrase, rootKey, bday)
|
||||||
pubPassphrase, privPassphrase, rootKey, bday, false,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateNewWalletExtendedKey creates a new wallet from an extended master root
|
// 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,
|
func (l *Loader) CreateNewWalletExtendedKey(pubPassphrase, privPassphrase []byte,
|
||||||
rootKey *hdkeychain.ExtendedKey, bday time.Time) (*Wallet, error) {
|
rootKey *hdkeychain.ExtendedKey, bday time.Time) (*Wallet, error) {
|
||||||
|
|
||||||
return l.createNewWallet(
|
return l.createNewWallet(pubPassphrase, privPassphrase, rootKey, bday)
|
||||||
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,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *Loader) createNewWallet(pubPassphrase, privPassphrase []byte,
|
func (l *Loader) createNewWallet(pubPassphrase, privPassphrase []byte,
|
||||||
rootKey *hdkeychain.ExtendedKey, bday time.Time,
|
rootKey *hdkeychain.ExtendedKey, bday time.Time) (*Wallet, error) {
|
||||||
isWatchingOnly bool) (*Wallet, error) {
|
|
||||||
|
|
||||||
defer l.mu.Unlock()
|
defer l.mu.Unlock()
|
||||||
l.mu.Lock()
|
l.mu.Lock()
|
||||||
|
@ -232,22 +216,12 @@ func (l *Loader) createNewWallet(pubPassphrase, privPassphrase []byte,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize the newly created database for the wallet before opening.
|
// Initialize the newly created database for the wallet before opening.
|
||||||
if isWatchingOnly {
|
err = CreateWithCallback(
|
||||||
err := CreateWatchingOnlyWithCallback(
|
l.db, pubPassphrase, privPassphrase, rootKey,
|
||||||
l.db, pubPassphrase, l.chainParams, bday,
|
l.chainParams, bday, l.walletCreated,
|
||||||
l.walletCreated,
|
)
|
||||||
)
|
if err != nil {
|
||||||
if err != nil {
|
return nil, err
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
err := CreateWithCallback(
|
|
||||||
l.db, pubPassphrase, privPassphrase, rootKey,
|
|
||||||
l.chainParams, bday, l.walletCreated,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open the newly-created wallet.
|
// Open the newly-created wallet.
|
||||||
|
|
|
@ -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(
|
witness, sigScript, err := w.ComputeInputScript(
|
||||||
tx, signOutput, idx, sigHashes, in.SighashType, nil,
|
tx, signOutput, idx, sigHashes, in.SighashType, nil,
|
||||||
)
|
)
|
||||||
|
|
|
@ -1177,16 +1177,14 @@ out:
|
||||||
// private key material, we need to prevent it from
|
// private key material, we need to prevent it from
|
||||||
// doing so while we are assembling the transaction.
|
// doing so while we are assembling the transaction.
|
||||||
release := func() {}
|
release := func() {}
|
||||||
if !w.Manager.WatchOnly() {
|
heldUnlock, err := w.holdUnlock()
|
||||||
heldUnlock, err := w.holdUnlock()
|
if err != nil {
|
||||||
if err != nil {
|
txr.resp <- createTxResponse{nil, err}
|
||||||
txr.resp <- createTxResponse{nil, err}
|
continue
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
release = heldUnlock.release
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
release = heldUnlock.release
|
||||||
|
|
||||||
tx, err := w.txToOutputs(
|
tx, err := w.txToOutputs(
|
||||||
txr.outputs, txr.keyScope, txr.account,
|
txr.outputs, txr.keyScope, txr.account,
|
||||||
txr.minconf, txr.feeSatPerKB,
|
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
|
// TODO: Each case will need updates when watch-only addrs
|
||||||
// is added. For P2PK, P2PKH, and P2SH, the address must be
|
// is added. For P2PK, P2PKH, and P2SH, the address must be
|
||||||
// looked up and not be watching-only. For multisig, all
|
// looked up.
|
||||||
// pubkeys must belong to the manager with the associated
|
// For multisig, all pubkeys must belong to the manager with
|
||||||
// private key (currently it only checks whether the pubkey
|
// the associated private key (currently it only checks whether
|
||||||
// exists, since the private key is required at the moment).
|
// the pubkey exists, since the private key is required at the
|
||||||
|
// moment).
|
||||||
var spendable bool
|
var spendable bool
|
||||||
scSwitch:
|
scSwitch:
|
||||||
switch sc {
|
switch sc {
|
||||||
|
@ -3199,13 +3198,6 @@ func (w *Wallet) SendOutputs(outputs []*wire.TxOut, keyScope *waddrmgr.KeyScope,
|
||||||
return nil, err
|
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)
|
txHash, err := w.reliablyPublishTransaction(createdTx.Tx, label)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -3695,19 +3687,7 @@ func CreateWithCallback(db walletdb.DB, pubPass, privPass []byte,
|
||||||
birthday time.Time, cb func(walletdb.ReadWriteTx) error) error {
|
birthday time.Time, cb func(walletdb.ReadWriteTx) error) error {
|
||||||
|
|
||||||
return create(
|
return create(
|
||||||
db, pubPass, privPass, rootKey, params, birthday, false, cb,
|
db, pubPass, privPass, rootKey, params, birthday, 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,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3719,31 +3699,16 @@ func Create(db walletdb.DB, pubPass, privPass []byte,
|
||||||
birthday time.Time) error {
|
birthday time.Time) error {
|
||||||
|
|
||||||
return create(
|
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,
|
func create(db walletdb.DB, pubPass, privPass []byte,
|
||||||
rootKey *hdkeychain.ExtendedKey, params *chaincfg.Params,
|
rootKey *hdkeychain.ExtendedKey, params *chaincfg.Params,
|
||||||
birthday time.Time, isWatchingOnly bool,
|
birthday time.Time,
|
||||||
cb func(walletdb.ReadWriteTx) error) error {
|
cb func(walletdb.ReadWriteTx) error) error {
|
||||||
|
|
||||||
// If no root key was provided, we create one now from a random seed.
|
// 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
|
if rootKey == nil {
|
||||||
// created individually from their xpubs.
|
|
||||||
if !isWatchingOnly && rootKey == nil {
|
|
||||||
hdSeed, err := hdkeychain.GenerateSeed(
|
hdSeed, err := hdkeychain.GenerateSeed(
|
||||||
hdkeychain.RecommendedSeedLen,
|
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.
|
// 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 " +
|
return fmt.Errorf("need extended private key for wallet that " +
|
||||||
"is not watching only")
|
"is not watching only")
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Add table
Reference in a new issue