waddrmgr: use correct DerivationPath for watch-only accounts

Previously, addresses that belong to a watch-only account would have a
derivation path using the internal account number used to identify
accounts within the databse, rather than the actual account number based
on the account's master public key child index. This wasn't an issue
before as only one account would exist within the wallet, the 0 account,
which is also the default. To ensure users of the DerivationPath struct
can arrive at addresses correctly, we introduce a new field
InternalAccount to denote the internal account number and repurpose the
existing Account field to its actual meaning.
This commit is contained in:
Wilmer Paulino 2021-02-16 17:01:12 -08:00
parent dead1a89d9
commit 0492cb4507
No known key found for this signature in database
GPG key ID: 6DF57B9F9514972F
8 changed files with 221 additions and 159 deletions

View file

@ -1774,7 +1774,9 @@ func validateAddress(icmd interface{}, w *wallet.Wallet) (interface{}, error) {
// The address lookup was successful which means there is further // The address lookup was successful which means there is further
// information about it available and it is "mine". // information about it available and it is "mine".
result.IsMine = true result.IsMine = true
acctName, err := w.AccountName(waddrmgr.KeyScopeBIP0044, ainfo.Account()) acctName, err := w.AccountName(
waddrmgr.KeyScopeBIP0044, ainfo.InternalAccount(),
)
if err != nil { if err != nil {
return nil, &ErrAccountNameNotFound return nil, &ErrAccountNameNotFound
} }

View file

@ -54,8 +54,8 @@ const (
// type may provide further fields to provide information specific to that type // type may provide further fields to provide information specific to that type
// of address. // of address.
type ManagedAddress interface { type ManagedAddress interface {
// Account returns the account the address is associated with. // Account returns the internal account the address is associated with.
Account() uint32 InternalAccount() uint32
// Address returns a btcutil.Address for the backing address. // Address returns a btcutil.Address for the backing address.
Address() btcutil.Address Address() btcutil.Address
@ -133,7 +133,7 @@ type managedAddress struct {
used bool used bool
addrType AddressType addrType AddressType
pubKey *btcec.PublicKey pubKey *btcec.PublicKey
privKeyEncrypted []byte privKeyEncrypted []byte // nil if part of watch-only account
privKeyCT []byte // non-nil if unlocked privKeyCT []byte // non-nil if unlocked
privKeyMutex sync.Mutex privKeyMutex sync.Mutex
} }
@ -177,11 +177,12 @@ func (a *managedAddress) lock() {
a.privKeyMutex.Unlock() a.privKeyMutex.Unlock()
} }
// Account returns the account number the address is associated with. // InternalAccount returns the internal account number the address is associated
// with.
// //
// This is part of the ManagedAddress interface implementation. // This is part of the ManagedAddress interface implementation.
func (a *managedAddress) Account() uint32 { func (a *managedAddress) InternalAccount() uint32 {
return a.derivationPath.Account return a.derivationPath.InternalAccount
} }
// AddrType returns the address type of the managed address. This can be used // AddrType returns the address type of the managed address. This can be used
@ -544,11 +545,11 @@ func (a *scriptAddress) lock() {
a.scriptMutex.Unlock() a.scriptMutex.Unlock()
} }
// Account returns the account the address is associated with. This will always // InternalAccount returns the account the address is associated with. This will
// be the ImportedAddrAccount constant for script addresses. // always be the ImportedAddrAccount constant for script addresses.
// //
// This is part of the ManagedAddress interface implementation. // This is part of the ManagedAddress interface implementation.
func (a *scriptAddress) Account() uint32 { func (a *scriptAddress) InternalAccount() uint32 {
return a.account return a.account
} }

View file

@ -13,6 +13,7 @@ import (
"time" "time"
"github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcutil/hdkeychain"
"github.com/btcsuite/btcwallet/walletdb" "github.com/btcsuite/btcwallet/walletdb"
_ "github.com/btcsuite/btcwallet/walletdb/bdb" _ "github.com/btcsuite/btcwallet/walletdb/bdb"
) )
@ -51,9 +52,10 @@ var (
privKey: hexToBytes("c27d6581b92785834b381fa697c4b0ffc4574b495743722e0acb7601b1b68b99"), privKey: hexToBytes("c27d6581b92785834b381fa697c4b0ffc4574b495743722e0acb7601b1b68b99"),
privKeyWIF: "L3jmpy54Pc7MLXTN2mL8Xas7BJziwKaUGmgnXXzgGbVRdiAniXZk", privKeyWIF: "L3jmpy54Pc7MLXTN2mL8Xas7BJziwKaUGmgnXXzgGbVRdiAniXZk",
derivationInfo: DerivationPath{ derivationInfo: DerivationPath{
Account: 0, InternalAccount: 0,
Branch: 0, Account: hdkeychain.HardenedKeyStart,
Index: 0, Branch: 0,
Index: 0,
}, },
}, },
{ {
@ -66,9 +68,10 @@ var (
privKey: hexToBytes("18f3b191019e83878a81557abebb2afda199e31d22e150d8bf4df4561671be6c"), privKey: hexToBytes("18f3b191019e83878a81557abebb2afda199e31d22e150d8bf4df4561671be6c"),
privKeyWIF: "Kx4DNid19W8sjNFN3uPqQE7UYnCqyEp7unCvdkf2LrVUFpnDtwpB", privKeyWIF: "Kx4DNid19W8sjNFN3uPqQE7UYnCqyEp7unCvdkf2LrVUFpnDtwpB",
derivationInfo: DerivationPath{ derivationInfo: DerivationPath{
Account: 0, InternalAccount: 0,
Branch: 0, Account: hdkeychain.HardenedKeyStart,
Index: 1, Branch: 0,
Index: 1,
}, },
}, },
{ {
@ -81,9 +84,10 @@ var (
privKey: hexToBytes("ccb8f6305b73136b363644b647f6efc0fd27b6b7d9c11c7e560662ed38db7b34"), privKey: hexToBytes("ccb8f6305b73136b363644b647f6efc0fd27b6b7d9c11c7e560662ed38db7b34"),
privKeyWIF: "L45fWF6Yd736fDohuB97vwRRLdQQJr3ZGvbokk9ubiT7aNrg7tTn", privKeyWIF: "L45fWF6Yd736fDohuB97vwRRLdQQJr3ZGvbokk9ubiT7aNrg7tTn",
derivationInfo: DerivationPath{ derivationInfo: DerivationPath{
Account: 0, InternalAccount: 0,
Branch: 0, Account: hdkeychain.HardenedKeyStart,
Index: 2, Branch: 0,
Index: 2,
}, },
}, },
{ {
@ -96,9 +100,10 @@ var (
privKey: hexToBytes("d6bc8ff768814fede2adcdb74826bd846924341b3862e3b6e31cdc084e992940"), privKey: hexToBytes("d6bc8ff768814fede2adcdb74826bd846924341b3862e3b6e31cdc084e992940"),
privKeyWIF: "L4R8XyxYQyPSpTwj8w96tM86a6j3QA9jbRPj3RA7DVTVWk71ndeP", privKeyWIF: "L4R8XyxYQyPSpTwj8w96tM86a6j3QA9jbRPj3RA7DVTVWk71ndeP",
derivationInfo: DerivationPath{ derivationInfo: DerivationPath{
Account: 0, InternalAccount: 0,
Branch: 0, Account: hdkeychain.HardenedKeyStart,
Index: 3, Branch: 0,
Index: 3,
}, },
}, },
{ {
@ -111,9 +116,10 @@ var (
privKey: hexToBytes("8563ade061110e03aee50695ffc5cb1c06c8310bde0a3674257c853c966968c0"), privKey: hexToBytes("8563ade061110e03aee50695ffc5cb1c06c8310bde0a3674257c853c966968c0"),
privKeyWIF: "L1h16Hunxomww4FrpyQP2iFmWNgG7U1u3awp6Vd3s2uGf7v5VU8c", privKeyWIF: "L1h16Hunxomww4FrpyQP2iFmWNgG7U1u3awp6Vd3s2uGf7v5VU8c",
derivationInfo: DerivationPath{ derivationInfo: DerivationPath{
Account: 0, InternalAccount: 0,
Branch: 0, Account: hdkeychain.HardenedKeyStart,
Index: 4, Branch: 0,
Index: 4,
}, },
}, },
{ {
@ -126,9 +132,10 @@ var (
privKey: hexToBytes("fe4f855fcf059ec6ddf7b25f63b19aa49c771d1fcb9850b68ae3d65e20657a60"), privKey: hexToBytes("fe4f855fcf059ec6ddf7b25f63b19aa49c771d1fcb9850b68ae3d65e20657a60"),
privKeyWIF: "L5k4HivqXvohxBMpuwD38iUgi6uewffwZny91ZNYfM39RXH2x3QR", privKeyWIF: "L5k4HivqXvohxBMpuwD38iUgi6uewffwZny91ZNYfM39RXH2x3QR",
derivationInfo: DerivationPath{ derivationInfo: DerivationPath{
Account: 0, InternalAccount: 0,
Branch: 1, Account: hdkeychain.HardenedKeyStart,
Index: 0, Branch: 1,
Index: 0,
}, },
}, },
{ {
@ -141,9 +148,10 @@ var (
privKey: hexToBytes("bfef521317c65b018ae7e6d7ecc3aa700d5d0f7ea84d567be9270382d0b5e3e6"), privKey: hexToBytes("bfef521317c65b018ae7e6d7ecc3aa700d5d0f7ea84d567be9270382d0b5e3e6"),
privKeyWIF: "L3eomUajnTDM3Pc8GU47qqXUFuCjvpqY7NYN9mH3x1ZFjDgiY4BU", privKeyWIF: "L3eomUajnTDM3Pc8GU47qqXUFuCjvpqY7NYN9mH3x1ZFjDgiY4BU",
derivationInfo: DerivationPath{ derivationInfo: DerivationPath{
Account: 0, InternalAccount: 0,
Branch: 1, Account: hdkeychain.HardenedKeyStart,
Index: 1, Branch: 1,
Index: 1,
}, },
}, },
{ {
@ -156,9 +164,10 @@ var (
privKey: hexToBytes("f506dffd4494c24006df7a35f3291f7ca0297a1a431557a1339bfed6f48738ca"), privKey: hexToBytes("f506dffd4494c24006df7a35f3291f7ca0297a1a431557a1339bfed6f48738ca"),
privKeyWIF: "L5S1bVQUPqQb1Su82fLoSpnGCjcPfdAQE1pJxWRopJSBdYNDHESv", privKeyWIF: "L5S1bVQUPqQb1Su82fLoSpnGCjcPfdAQE1pJxWRopJSBdYNDHESv",
derivationInfo: DerivationPath{ derivationInfo: DerivationPath{
Account: 0, InternalAccount: 0,
Branch: 1, Account: hdkeychain.HardenedKeyStart,
Index: 2, Branch: 1,
Index: 2,
}, },
}, },
{ {
@ -171,9 +180,10 @@ var (
privKey: hexToBytes("b3629de8ef6a275b4ffae41aa2bbbc2952eb92282ea6402435abbb010ecc1fb8"), privKey: hexToBytes("b3629de8ef6a275b4ffae41aa2bbbc2952eb92282ea6402435abbb010ecc1fb8"),
privKeyWIF: "L3EQsGeEnyXmKaux54cG4DQeCSQDvGuvEuy3W2ss4geum7AtWaHw", privKeyWIF: "L3EQsGeEnyXmKaux54cG4DQeCSQDvGuvEuy3W2ss4geum7AtWaHw",
derivationInfo: DerivationPath{ derivationInfo: DerivationPath{
Account: 0, InternalAccount: 0,
Branch: 1, Account: hdkeychain.HardenedKeyStart,
Index: 3, Branch: 1,
Index: 3,
}, },
}, },
{ {
@ -186,9 +196,10 @@ var (
privKey: hexToBytes("ca747a7ef815ea0dbe68655272cecbfbd65f2a109019a9ed28e0d3dcaffe05c3"), privKey: hexToBytes("ca747a7ef815ea0dbe68655272cecbfbd65f2a109019a9ed28e0d3dcaffe05c3"),
privKeyWIF: "L41Frac75RPbTELKzw1EGC2qCkdveiVumpmsyX4daAvyyCMxit1W", privKeyWIF: "L41Frac75RPbTELKzw1EGC2qCkdveiVumpmsyX4daAvyyCMxit1W",
derivationInfo: DerivationPath{ derivationInfo: DerivationPath{
Account: 0, InternalAccount: 0,
Branch: 1, Account: hdkeychain.HardenedKeyStart,
Index: 4, Branch: 1,
Index: 4,
}, },
}, },
} }

View file

@ -1178,9 +1178,9 @@ func (m *Manager) Unlock(ns walletdb.ReadBucket, passphrase []byte) error {
// We'll also derive any private keys that are pending due to // We'll also derive any private keys that are pending due to
// them being created while the address manager was locked. // them being created while the address manager was locked.
for _, info := range manager.deriveOnUnlock { for _, info := range manager.deriveOnUnlock {
addressKey, err := manager.deriveKeyFromPath( addressKey, _, err := manager.deriveKeyFromPath(
ns, info.managedAddr.Account(), info.branch, ns, info.managedAddr.InternalAccount(),
info.index, true, info.branch, info.index, true,
) )
if err != nil { if err != nil {
m.lock() m.lock()

View file

@ -71,15 +71,15 @@ func failingSecretKeyGen(passphrase *[]byte,
// blocks have been inserted and therefore some of the transaction outputs are // blocks have been inserted and therefore some of the transaction outputs are
// spent. // spent.
type testContext struct { type testContext struct {
t *testing.T t *testing.T
caseName string caseName string
db walletdb.DB db walletdb.DB
rootManager *Manager rootManager *Manager
manager *ScopedKeyManager manager *ScopedKeyManager
account uint32 internalAccount uint32
create bool create bool
unlocked bool unlocked bool
watchingOnly bool watchingOnly bool
} }
// addrType is the type of address being tested // addrType is the type of address being tested
@ -114,7 +114,7 @@ func testNamePrefix(tc *testContext) string {
prefix = "Create " prefix = "Create "
} }
return fmt.Sprintf("(%s) %s account #%d", tc.caseName, prefix, tc.account) return fmt.Sprintf("(%s) %s account #%d", tc.caseName, prefix, tc.internalAccount)
} }
// testManagedPubKeyAddress ensures the data returned by all exported functions // testManagedPubKeyAddress ensures the data returned by all exported functions
@ -293,9 +293,9 @@ func testManagedScriptAddress(tc *testContext, prefix string,
func testAddress(tc *testContext, prefix string, gotAddr ManagedAddress, func testAddress(tc *testContext, prefix string, gotAddr ManagedAddress,
wantAddr *expectedAddr) bool { wantAddr *expectedAddr) bool {
if gotAddr.Account() != tc.account { if gotAddr.InternalAccount() != tc.internalAccount {
tc.t.Errorf("ManagedAddress.Account: unexpected account - got "+ tc.t.Errorf("ManagedAddress.Account: unexpected account - got "+
"%d, want %d", gotAddr.Account(), tc.account) "%d, want %d", gotAddr.InternalAccount(), tc.internalAccount)
return false return false
} }
@ -360,7 +360,9 @@ func testExternalAddresses(tc *testContext) bool {
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)
var err error var err error
addrs, err = tc.manager.NextExternalAddresses(ns, tc.account, 5) addrs, err = tc.manager.NextExternalAddresses(
ns, tc.internalAccount, 5,
)
return err return err
}) })
if err != nil { if err != nil {
@ -395,7 +397,9 @@ func testExternalAddresses(tc *testContext) bool {
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)
var err error var err error
lastAddr, err = tc.manager.LastExternalAddress(ns, tc.account) lastAddr, err = tc.manager.LastExternalAddress(
ns, tc.internalAccount,
)
return err return err
}) })
if err != nil { if err != nil {
@ -512,7 +516,9 @@ func testInternalAddresses(tc *testContext) bool {
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)
var err error var err error
addrs, err = tc.manager.NextInternalAddresses(ns, tc.account, 5) addrs, err = tc.manager.NextInternalAddresses(
ns, tc.internalAccount, 5,
)
return err return err
}) })
if err != nil { if err != nil {
@ -547,7 +553,9 @@ func testInternalAddresses(tc *testContext) bool {
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)
var err error var err error
lastAddr, err = tc.manager.LastInternalAddress(ns, tc.account) lastAddr, err = tc.manager.LastInternalAddress(
ns, tc.internalAccount,
)
return err return err
}) })
if err != nil { if err != nil {
@ -776,7 +784,7 @@ func testImportPrivateKey(tc *testContext) bool {
} }
// 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.account = ImportedAddrAccount tc.internalAccount = ImportedAddrAccount
prefix := testNamePrefix(tc) + " testImportPrivateKey" prefix := testNamePrefix(tc) + " testImportPrivateKey"
if tc.create { if tc.create {
for i, test := range tests { for i, test := range tests {
@ -949,7 +957,7 @@ func testImportScript(tc *testContext) bool {
} }
// Only import the scripts when in the create phase of testing. // Only import the scripts when in the create phase of testing.
tc.account = ImportedAddrAccount tc.internalAccount = ImportedAddrAccount
prefix := testNamePrefix(tc) prefix := testNamePrefix(tc)
if tc.create { if tc.create {
for i, test := range tests { for i, test := range tests {
@ -1320,7 +1328,7 @@ func testNewAccount(tc *testContext) bool {
tc.unlocked = true tc.unlocked = true
testName := "acct-create" testName := "acct-create"
expectedAccount := tc.account + 1 expectedAccount := tc.internalAccount + 1
if !tc.create { if !tc.create {
// Create a new account in open mode // Create a new account in open mode
testName = "acct-open" testName = "acct-open"
@ -1480,7 +1488,7 @@ func testRenameAccount(tc *testContext) bool {
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)
var err error var err error
acctName, err = tc.manager.AccountName(ns, tc.account) acctName, err = tc.manager.AccountName(ns, tc.internalAccount)
return err return err
}) })
if err != nil { if err != nil {
@ -1490,7 +1498,7 @@ func testRenameAccount(tc *testContext) bool {
testName := acctName + "-renamed" testName := acctName + "-renamed"
err = walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) error { err = walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) error {
ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) ns := tx.ReadWriteBucket(waddrmgrNamespaceKey)
return tc.manager.RenameAccount(ns, tc.account, testName) return tc.manager.RenameAccount(ns, tc.internalAccount, testName)
}) })
if err != nil { if err != nil {
tc.t.Errorf("RenameAccount: unexpected error: %v", err) tc.t.Errorf("RenameAccount: unexpected error: %v", err)
@ -1500,7 +1508,7 @@ func testRenameAccount(tc *testContext) bool {
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)
var err error var err error
newName, err = tc.manager.AccountName(ns, tc.account) newName, err = tc.manager.AccountName(ns, tc.internalAccount)
return err return err
}) })
if err != nil { if err != nil {
@ -1516,7 +1524,7 @@ func testRenameAccount(tc *testContext) bool {
// Test duplicate account name error // Test duplicate account name error
err = walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) error { err = walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) error {
ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) ns := tx.ReadWriteBucket(waddrmgrNamespaceKey)
return tc.manager.RenameAccount(ns, tc.account, testName) return tc.manager.RenameAccount(ns, tc.internalAccount, testName)
}) })
wantErrCode := ErrDuplicateAccount wantErrCode := ErrDuplicateAccount
if !checkManagerError(tc.t, testName, err, wantErrCode) { if !checkManagerError(tc.t, testName, err, wantErrCode) {
@ -1587,7 +1595,7 @@ func testForEachAccountAddress(tc *testContext) bool {
var addrs []ManagedAddress var addrs []ManagedAddress
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.manager.ForEachAccountAddress(ns, tc.account, return tc.manager.ForEachAccountAddress(ns, tc.internalAccount,
func(maddr ManagedAddress) error { func(maddr ManagedAddress) error {
addrs = append(addrs, maddr) addrs = append(addrs, maddr)
return nil return nil
@ -1632,14 +1640,14 @@ func testManagerAPI(tc *testContext, caseCreatedWatchingOnly bool) {
testChangePassphrase(tc) testChangePassphrase(tc)
// Reset default account // Reset default account
tc.account = 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.account = 1 tc.internalAccount = 1
testRenameAccount(tc) testRenameAccount(tc)
} else { } else {
// Test API for created watch-only case. // Test API for created watch-only case.
@ -1720,14 +1728,14 @@ func testConvertWatchingOnly(tc *testContext) bool {
return false return false
} }
testManagerAPI(&testContext{ testManagerAPI(&testContext{
t: tc.t, t: tc.t,
caseName: tc.caseName, caseName: tc.caseName,
db: db, db: db,
rootManager: mgr, rootManager: mgr,
manager: scopedMgr, manager: scopedMgr,
account: 0, internalAccount: 0,
create: false, create: false,
watchingOnly: true, watchingOnly: true,
}, false) }, false)
mgr.Close() mgr.Close()
@ -1751,14 +1759,14 @@ func testConvertWatchingOnly(tc *testContext) bool {
} }
testManagerAPI(&testContext{ testManagerAPI(&testContext{
t: tc.t, t: tc.t,
caseName: tc.caseName, caseName: tc.caseName,
db: db, db: db,
rootManager: mgr, rootManager: mgr,
manager: scopedMgr, manager: scopedMgr,
account: 0, internalAccount: 0,
create: false, create: false,
watchingOnly: true, watchingOnly: true,
}, false) }, false)
return true return true
@ -1951,14 +1959,14 @@ func testManagerCase(t *testing.T, caseName string,
// 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{
t: t, t: t,
caseName: caseName, caseName: caseName,
db: db, db: db,
manager: scopedMgr, manager: scopedMgr,
rootManager: mgr, rootManager: mgr,
account: 0, internalAccount: 0,
create: true, create: true,
watchingOnly: caseCreatedWatchingOnly, watchingOnly: caseCreatedWatchingOnly,
}, caseCreatedWatchingOnly) }, caseCreatedWatchingOnly)
mgr.Close() mgr.Close()
@ -1981,14 +1989,14 @@ 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)
} }
tc := &testContext{ tc := &testContext{
t: t, t: t,
caseName: caseName, caseName: caseName,
db: db, db: db,
manager: scopedMgr, manager: scopedMgr,
rootManager: mgr, rootManager: mgr,
account: 0, internalAccount: 0,
create: false, create: false,
watchingOnly: caseCreatedWatchingOnly, watchingOnly: caseCreatedWatchingOnly,
} }
testManagerAPI(tc, caseCreatedWatchingOnly) testManagerAPI(tc, caseCreatedWatchingOnly)
@ -2743,9 +2751,10 @@ func testNewRawAccount(t *testing.T, mgr *Manager, db walletdb.DB,
ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) ns := tx.ReadWriteBucket(waddrmgrNamespaceKey)
keyPath := DerivationPath{ keyPath := DerivationPath{
Account: accountNum, InternalAccount: accountNum,
Branch: 0, Account: hdkeychain.HardenedKeyStart,
Index: 0, Branch: 0,
Index: 0,
} }
accountTargetAddr, err = scopedMgr.DeriveFromKeyPath( accountTargetAddr, err = scopedMgr.DeriveFromKeyPath(
ns, keyPath, ns, keyPath,

View file

@ -23,6 +23,10 @@ import (
// m/purpose'/cointype'/account/branch/index, where purpose' and cointype' are // m/purpose'/cointype'/account/branch/index, where purpose' and cointype' are
// bound by the scope of a particular manager. // bound by the scope of a particular manager.
type DerivationPath struct { type DerivationPath struct {
// InternalAccount is the internal account number used within the
// wallet's database to identify accounts.
InternalAccount uint32
// Account is the account, or the first immediate child from the scoped // Account is the account, or the first immediate child from the scoped
// manager's hardened coin type key. // manager's hardened coin type key.
Account uint32 Account uint32
@ -213,21 +217,15 @@ func (s *ScopedKeyManager) Close() {
// //
// This function MUST be called with the manager lock held for writes. // This function MUST be called with the manager lock held for writes.
func (s *ScopedKeyManager) keyToManaged(derivedKey *hdkeychain.ExtendedKey, func (s *ScopedKeyManager) keyToManaged(derivedKey *hdkeychain.ExtendedKey,
account, branch, index uint32) (ManagedAddress, error) { derivationPath DerivationPath) (ManagedAddress, error) {
var addrType AddressType var addrType AddressType
if branch == InternalBranch { if derivationPath.Branch == InternalBranch {
addrType = s.addrSchema.InternalAddrType addrType = s.addrSchema.InternalAddrType
} else { } else {
addrType = s.addrSchema.ExternalAddrType addrType = s.addrSchema.ExternalAddrType
} }
derivationPath := DerivationPath{
Account: account,
Branch: branch,
Index: index,
}
// Create a new managed address based on the public or private key // Create a new managed address based on the public or private key
// depending on whether the passed key is private. Also, zero the key // depending on whether the passed key is private. Also, zero the key
// after creating the managed address from it. // after creating the managed address from it.
@ -245,13 +243,13 @@ func (s *ScopedKeyManager) keyToManaged(derivedKey *hdkeychain.ExtendedKey,
// unlocked. // unlocked.
info := unlockDeriveInfo{ info := unlockDeriveInfo{
managedAddr: ma, managedAddr: ma,
branch: branch, branch: derivationPath.Branch,
index: index, index: derivationPath.Index,
} }
s.deriveOnUnlock = append(s.deriveOnUnlock, &info) s.deriveOnUnlock = append(s.deriveOnUnlock, &info)
} }
if branch == InternalBranch { if derivationPath.Branch == InternalBranch {
ma.internal = true ma.internal = true
} }
@ -343,7 +341,9 @@ func (s *ScopedKeyManager) loadAccountInfo(ns walletdb.ReadBucket,
nextInternalIndex: row.nextInternalIndex, nextInternalIndex: row.nextInternalIndex,
} }
if !s.rootManager.isLocked() { watchOnly := s.rootManager.watchOnly() || len(acctInfo.acctKeyEncrypted) == 0
private := !s.rootManager.isLocked() && !watchOnly
if private {
// Use the crypto private key to decrypt the account private // Use the crypto private key to decrypt the account private
// extended keys. // extended keys.
decrypted, err := s.rootManager.cryptoKeyPriv.Decrypt(acctInfo.acctKeyEncrypted) decrypted, err := s.rootManager.cryptoKeyPriv.Decrypt(acctInfo.acctKeyEncrypted)
@ -367,13 +367,17 @@ func (s *ScopedKeyManager) loadAccountInfo(ns walletdb.ReadBucket,
if index > 0 { if index > 0 {
index-- index--
} }
lastExtKey, err := s.deriveKey( lastExtAddrPath := DerivationPath{
acctInfo, branch, index, !s.rootManager.isLocked(), InternalAccount: account,
) Account: acctKeyPub.ChildIndex(),
Branch: branch,
Index: index,
}
lastExtKey, err := s.deriveKey(acctInfo, branch, index, private)
if err != nil { if err != nil {
return nil, err return nil, err
} }
lastExtAddr, err := s.keyToManaged(lastExtKey, account, branch, index) lastExtAddr, err := s.keyToManaged(lastExtKey, lastExtAddrPath)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -384,13 +388,17 @@ func (s *ScopedKeyManager) loadAccountInfo(ns walletdb.ReadBucket,
if index > 0 { if index > 0 {
index-- index--
} }
lastIntKey, err := s.deriveKey( lastIntAddrPath := DerivationPath{
acctInfo, branch, index, !s.rootManager.isLocked(), InternalAccount: account,
) Account: acctKeyPub.ChildIndex(),
Branch: branch,
Index: index,
}
lastIntKey, err := s.deriveKey(acctInfo, branch, index, private)
if err != nil { if err != nil {
return nil, err return nil, err
} }
lastIntAddr, err := s.keyToManaged(lastIntKey, account, branch, index) lastIntAddr, err := s.keyToManaged(lastIntKey, lastIntAddrPath)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -452,36 +460,55 @@ func (s *ScopedKeyManager) AccountProperties(ns walletdb.ReadBucket,
// DeriveFromKeyPath attempts to derive a maximal child key (under the BIP0044 // DeriveFromKeyPath attempts to derive a maximal child key (under the BIP0044
// scheme) from a given key path. If key derivation isn't possible, then an // scheme) from a given key path. If key derivation isn't possible, then an
// error will be returned. // error will be returned.
//
// NOTE: The key will be derived from the account stored in the database under
// the InternalAccount number.
func (s *ScopedKeyManager) DeriveFromKeyPath(ns walletdb.ReadBucket, func (s *ScopedKeyManager) DeriveFromKeyPath(ns walletdb.ReadBucket,
kp DerivationPath) (ManagedAddress, error) { kp DerivationPath) (ManagedAddress, error) {
s.mtx.Lock() s.mtx.Lock()
defer s.mtx.Unlock() defer s.mtx.Unlock()
extKey, err := s.deriveKeyFromPath( watchOnly := s.rootManager.WatchOnly()
ns, kp.Account, kp.Branch, kp.Index, !s.rootManager.IsLocked(), private := !s.rootManager.IsLocked() && !watchOnly
addrKey, _, err := s.deriveKeyFromPath(
ns, kp.InternalAccount, kp.Branch, kp.Index, private,
) )
if err != nil { if err != nil {
return nil, err return nil, err
} }
return s.keyToManaged(extKey, kp.Account, kp.Branch, kp.Index) return s.keyToManaged(addrKey, kp)
} }
// deriveKeyFromPath returns either a public or private derived extended key // deriveKeyFromPath returns either a public or private derived extended key
// based on the private flag for the given an account, branch, and index. // based on the private flag for an address given an account, branch, and index.
// The account master key is also returned.
// //
// This function MUST be called with the manager lock held for writes. // This function MUST be called with the manager lock held for writes.
func (s *ScopedKeyManager) deriveKeyFromPath(ns walletdb.ReadBucket, account, branch, func (s *ScopedKeyManager) deriveKeyFromPath(ns walletdb.ReadBucket,
index uint32, private bool) (*hdkeychain.ExtendedKey, error) { internalAccount, branch, index uint32, private bool) (
*hdkeychain.ExtendedKey, *hdkeychain.ExtendedKey, error) {
// Look up the account key information. // Look up the account key information.
acctInfo, err := s.loadAccountInfo(ns, account) acctInfo, err := s.loadAccountInfo(ns, internalAccount)
if err != nil { if err != nil {
return nil, err return nil, nil, err
}
private = private && acctInfo.acctKeyPriv != nil
addrKey, err := s.deriveKey(acctInfo, branch, index, private)
if err != nil {
return nil, nil, err
} }
return s.deriveKey(acctInfo, branch, index, private) acctKey := acctInfo.acctKeyPub
if private {
acctKey = acctInfo.acctKeyPriv
}
return addrKey, acctKey, nil
} }
// chainAddressRowToManaged returns a new managed address based on chained // chainAddressRowToManaged returns a new managed address based on chained
@ -493,16 +520,22 @@ 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.
isLocked := s.rootManager.isLocked() private := !s.rootManager.isLocked() && !s.rootManager.watchOnly()
addressKey, err := s.deriveKeyFromPath( addressKey, acctKey, err := s.deriveKeyFromPath(
ns, row.account, row.branch, row.index, !isLocked, ns, row.account, row.branch, row.index, private,
) )
if err != nil { if err != nil {
return nil, err return nil, err
} }
return s.keyToManaged(
return s.keyToManaged(addressKey, row.account, row.branch, row.index) addressKey, DerivationPath{
InternalAccount: row.account,
Account: acctKey.ChildIndex(),
Branch: row.branch,
Index: row.index,
},
)
} }
// importedAddressRowToManaged returns a new managed address based on imported // importedAddressRowToManaged returns a new managed address based on imported
@ -525,7 +558,7 @@ func (s *ScopedKeyManager) importedAddressRowToManaged(row *dbImportedAddressRow
// Since this is an imported address, we won't populate the full // Since this is an imported address, we won't populate the full
// derivation path, as we don't have enough information to do so. // derivation path, as we don't have enough information to do so.
derivationPath := DerivationPath{ derivationPath := DerivationPath{
Account: row.account, InternalAccount: row.account,
} }
compressed := len(pubBytes) == btcec.PubKeyBytesLenCompressed compressed := len(pubBytes) == btcec.PubKeyBytesLenCompressed
@ -688,7 +721,8 @@ 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
if !s.rootManager.IsLocked() { watchOnly := s.rootManager.WatchOnly() || len(acctInfo.acctKeyEncrypted) == 0
if !s.rootManager.IsLocked() && !watchOnly {
acctKey = acctInfo.acctKeyPriv acctKey = acctInfo.acctKeyPriv
} }
@ -757,9 +791,10 @@ func (s *ScopedKeyManager) nextAddresses(ns walletdb.ReadWriteBucket,
// proper derivation path so this information can be available // proper derivation path so this information can be available
// to callers. // to callers.
derivationPath := DerivationPath{ derivationPath := DerivationPath{
Account: account, InternalAccount: account,
Branch: branchNum, Account: acctKey.ChildIndex(),
Index: nextIndex - 1, Branch: branchNum,
Index: nextIndex - 1,
} }
// Create a new managed address based on the public or private // Create a new managed address based on the public or private
@ -843,7 +878,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() && !s.rootManager.watchOnly() { if s.rootManager.isLocked() && !watchOnly {
s.deriveOnUnlock = append(s.deriveOnUnlock, info) s.deriveOnUnlock = append(s.deriveOnUnlock, info)
} }
} }
@ -883,7 +918,8 @@ 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
if !s.rootManager.IsLocked() { watchOnly := s.rootManager.WatchOnly() || acctInfo.acctKeyPriv != nil
if !s.rootManager.IsLocked() && !watchOnly {
acctKey = acctInfo.acctKeyPriv acctKey = acctInfo.acctKeyPriv
} }
@ -959,9 +995,10 @@ func (s *ScopedKeyManager) extendAddresses(ns walletdb.ReadWriteBucket,
// proper derivation path so this information can be available // proper derivation path so this information can be available
// to callers. // to callers.
derivationPath := DerivationPath{ derivationPath := DerivationPath{
Account: account, InternalAccount: account,
Branch: branchNum, Account: acctInfo.acctKeyPub.ChildIndex(),
Index: nextIndex - 1, Branch: branchNum,
Index: nextIndex - 1,
} }
// Create a new managed address based on the public or private // Create a new managed address based on the public or private
@ -1031,7 +1068,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() && !s.rootManager.WatchOnly() { if s.rootManager.IsLocked() && !watchOnly {
s.deriveOnUnlock = append(s.deriveOnUnlock, info) s.deriveOnUnlock = append(s.deriveOnUnlock, info)
} }
} }
@ -1539,7 +1576,7 @@ func (s *ScopedKeyManager) ImportPrivateKey(ns walletdb.ReadWriteBucket,
// The full derivation path for an imported key is incomplete as we // The full derivation path for an imported key is incomplete as we
// don't know exactly how it was derived. // don't know exactly how it was derived.
importedDerivationPath := DerivationPath{ importedDerivationPath := DerivationPath{
Account: ImportedAddrAccount, InternalAccount: ImportedAddrAccount,
} }
// Create a new managed address based on the imported address. // Create a new managed address based on the imported address.
@ -1575,7 +1612,7 @@ func (s *ScopedKeyManager) ImportPublicKey(ns walletdb.ReadWriteBucket,
// The full derivation path for an imported key is incomplete as we // The full derivation path for an imported key is incomplete as we
// don't know exactly how it was derived. // don't know exactly how it was derived.
importedDerivationPath := DerivationPath{ importedDerivationPath := DerivationPath{
Account: ImportedAddrAccount, InternalAccount: ImportedAddrAccount,
} }
return s.toPublicManagedAddress( return s.toPublicManagedAddress(
pubKey, true, true, importedDerivationPath, pubKey, true, true, importedDerivationPath,
@ -1724,7 +1761,7 @@ func (s *ScopedKeyManager) ImportScript(ns walletdb.ReadWriteBucket,
defer s.mtx.Unlock() defer s.mtx.Unlock()
// The manager must be unlocked to encrypt the imported script. // The manager must be unlocked to encrypt the imported script.
if s.rootManager.IsLocked() && !s.rootManager.WatchOnly() { if s.rootManager.IsLocked() {
return nil, managerError(ErrLocked, errLocked, nil) return nil, managerError(ErrLocked, errLocked, nil)
} }

View file

@ -90,7 +90,7 @@ func lookupOutputChain(dbtx walletdb.ReadTx, w *Wallet, details *wtxmgr.TxDetail
if err != nil { if err != nil {
log.Errorf("Cannot fetch account for wallet output: %v", err) log.Errorf("Cannot fetch account for wallet output: %v", err)
} else { } else {
account = ma.Account() account = ma.InternalAccount()
internal = ma.Internal() internal = ma.Internal()
} }
return return

View file

@ -946,18 +946,20 @@ func expandScopeHorizons(ns walletdb.ReadWriteBucket,
// externalKeyPath returns the relative external derivation path /0/0/index. // externalKeyPath returns the relative external derivation path /0/0/index.
func externalKeyPath(index uint32) waddrmgr.DerivationPath { func externalKeyPath(index uint32) waddrmgr.DerivationPath {
return waddrmgr.DerivationPath{ return waddrmgr.DerivationPath{
Account: waddrmgr.DefaultAccountNum, InternalAccount: waddrmgr.DefaultAccountNum,
Branch: waddrmgr.ExternalBranch, Account: waddrmgr.DefaultAccountNum,
Index: index, Branch: waddrmgr.ExternalBranch,
Index: index,
} }
} }
// internalKeyPath returns the relative internal derivation path /0/1/index. // internalKeyPath returns the relative internal derivation path /0/1/index.
func internalKeyPath(index uint32) waddrmgr.DerivationPath { func internalKeyPath(index uint32) waddrmgr.DerivationPath {
return waddrmgr.DerivationPath{ return waddrmgr.DerivationPath{
Account: waddrmgr.DefaultAccountNum, InternalAccount: waddrmgr.DefaultAccountNum,
Branch: waddrmgr.InternalBranch, Account: waddrmgr.DefaultAccountNum,
Index: index, Branch: waddrmgr.InternalBranch,
Index: index,
} }
} }