From 6ff982ccdc37867bade1a295d797ae145847833c Mon Sep 17 00:00:00 2001 From: Wilmer Paulino Date: Mon, 22 Feb 2021 17:38:28 -0800 Subject: [PATCH 01/16] wtxmgr: prevent race condition by using local vars --- wtxmgr/tx_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wtxmgr/tx_test.go b/wtxmgr/tx_test.go index 344b905..790112f 100644 --- a/wtxmgr/tx_test.go +++ b/wtxmgr/tx_test.go @@ -2284,11 +2284,11 @@ func TestTxLabel(t *testing.T) { defer teardown() // txid is the transaction hash we will use to write and get labels for. - txid := TstRecvTx.Hash() + txid := &chainhash.Hash{1} // txidNotFound is distinct from txid, and will not have a label written // to disk. - txidNotFound := TstSpendingTx.Hash() + txidNotFound := &chainhash.Hash{2} // getBucket gets the top level bucket, and fails the test if it is // not found. From 742d8793520e0a262ec8732d752676a6952ec066 Mon Sep 17 00:00:00 2001 From: Wilmer Paulino Date: Mon, 22 Feb 2021 17:52:30 -0800 Subject: [PATCH 02/16] multi: fix linter --- rpc/legacyrpc/rpcserver_test.go | 3 ++- rpc/walletrpc/api.pb.go | 10 ++++++---- waddrmgr/manager.go | 6 +++--- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/rpc/legacyrpc/rpcserver_test.go b/rpc/legacyrpc/rpcserver_test.go index bb4e0b1..b41ee1a 100644 --- a/rpc/legacyrpc/rpcserver_test.go +++ b/rpc/legacyrpc/rpcserver_test.go @@ -26,7 +26,8 @@ func TestThrottle(t *testing.T) { go func() { res, err := http.Get(srv.URL) if err != nil { - t.Fatal(err) + t.Log(err) + return } codes <- res.StatusCode }() diff --git a/rpc/walletrpc/api.pb.go b/rpc/walletrpc/api.pb.go index a0232bb..e6716e4 100644 --- a/rpc/walletrpc/api.pb.go +++ b/rpc/walletrpc/api.pb.go @@ -1210,10 +1210,12 @@ type SpentnessNotificationsResponse struct { Spender *SpentnessNotificationsResponse_Spender `protobuf:"bytes,3,opt,name=spender" json:"spender,omitempty"` } -func (m *SpentnessNotificationsResponse) Reset() { *m = SpentnessNotificationsResponse{} } -func (m *SpentnessNotificationsResponse) String() string { return proto.CompactTextString(m) } -func (*SpentnessNotificationsResponse) ProtoMessage() {} -func (*SpentnessNotificationsResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{36} } +func (m *SpentnessNotificationsResponse) Reset() { *m = SpentnessNotificationsResponse{} } +func (m *SpentnessNotificationsResponse) String() string { return proto.CompactTextString(m) } +func (*SpentnessNotificationsResponse) ProtoMessage() {} +func (*SpentnessNotificationsResponse) Descriptor() ([]byte, []int) { + return fileDescriptor0, []int{36} +} func (m *SpentnessNotificationsResponse) GetTransactionHash() []byte { if m != nil { diff --git a/waddrmgr/manager.go b/waddrmgr/manager.go index 4dd4cae..45ee067 100644 --- a/waddrmgr/manager.go +++ b/waddrmgr/manager.go @@ -1783,10 +1783,10 @@ func Create(ns walletdb.ReadWriteBucket, pubParams := masterKeyPub.Marshal() - var privParams []byte = nil + var privParams []byte var masterKeyPriv *snacl.SecretKey - var cryptoKeyPrivEnc []byte = nil - var cryptoKeyScriptEnc []byte = nil + var cryptoKeyPrivEnc []byte + var cryptoKeyScriptEnc []byte if !isWatchingOnly { masterKeyPriv, err = newSecretKey(&privPassphrase, config) if err != nil { From 967f663f84a96676d89024edd81ecdc12c20e1f4 Mon Sep 17 00:00:00 2001 From: Wilmer Paulino Date: Mon, 22 Feb 2021 17:52:37 -0800 Subject: [PATCH 03/16] travis: bump go version to 1.15.x --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 7135b4e..3c204e3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: go go: - - "1.13.x" + - "1.15.x" sudo: false install: - GO111MODULE=on go install -v ./... From 02c4a1a54f031e64599121346e59b07d53f8001b Mon Sep 17 00:00:00 2001 From: Wilmer Paulino Date: Tue, 16 Feb 2021 17:01:02 -0800 Subject: [PATCH 04/16] build: update btcutil dependency --- go.mod | 8 ++++---- go.sum | 13 +++++++++++-- waddrmgr/manager.go | 18 ++++++++++++------ waddrmgr/scoped_manager.go | 12 ++++++------ 4 files changed, 33 insertions(+), 18 deletions(-) diff --git a/go.mod b/go.mod index b9d1b4d..dae8619 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,8 @@ module github.com/btcsuite/btcwallet require ( github.com/btcsuite/btcd v0.20.1-beta.0.20200513120220-b470eee47728 github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f - github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d - github.com/btcsuite/btcutil/psbt v1.0.3-0.20200826194809-5f93e33af2b0 + github.com/btcsuite/btcutil v1.0.3-0.20201208143702-a53e38424cce + github.com/btcsuite/btcutil/psbt v1.0.3-0.20201208143702-a53e38424cce github.com/btcsuite/btcwallet/wallet/txauthor v1.0.0 github.com/btcsuite/btcwallet/wallet/txrules v1.0.0 github.com/btcsuite/btcwallet/walletdb v1.3.4 @@ -17,8 +17,8 @@ require ( github.com/kkdai/bstream v0.0.0-20181106074824-b3251f7901ec // indirect github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf github.com/lightninglabs/neutrino v0.11.0 - golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67 - golang.org/x/net v0.0.0-20190206173232-65e2d4e15006 + golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d + golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 google.golang.org/genproto v0.0.0-20190201180003-4b09977fb922 // indirect google.golang.org/grpc v1.18.0 ) diff --git a/go.sum b/go.sum index 3625f4a..d342646 100644 --- a/go.sum +++ b/go.sum @@ -11,8 +11,10 @@ github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9 github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d h1:yJzD/yFppdVCf6ApMkVy8cUxV0XrxdP9rVf6D87/Mng= github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= -github.com/btcsuite/btcutil/psbt v1.0.3-0.20200826194809-5f93e33af2b0 h1:3Zumkyl6PWyHuVJ04me0xeD9CnPOhNgeGpapFbzy7O4= -github.com/btcsuite/btcutil/psbt v1.0.3-0.20200826194809-5f93e33af2b0/go.mod h1:LVveMu4VaNSkIRTZu2+ut0HDBRuYjqGocxDMNS1KuGQ= +github.com/btcsuite/btcutil v1.0.3-0.20201208143702-a53e38424cce h1:YtWJF7RHm2pYCvA5t0RPmAaLUhREsKuKd+SLhxFbFeQ= +github.com/btcsuite/btcutil v1.0.3-0.20201208143702-a53e38424cce/go.mod h1:0DVlHczLPewLcPGEIeUEzfOJhqGPQ0mJJRDBtD307+o= +github.com/btcsuite/btcutil/psbt v1.0.3-0.20201208143702-a53e38424cce h1:3PRwz+js0AMMV1fHRrCdQ55akoomx4Q3ulozHC3BDDY= +github.com/btcsuite/btcutil/psbt v1.0.3-0.20201208143702-a53e38424cce/go.mod h1:LVveMu4VaNSkIRTZu2+ut0HDBRuYjqGocxDMNS1KuGQ= github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd h1:R/opQEbFEy9JGkIguV40SvRY1uliPX8ifOvi6ICsFCw= github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY= @@ -85,6 +87,9 @@ golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44 h1:9lP3x0pW80sDI6t1UMSLA4 golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67 h1:ng3VDlRp5/DHpSWl02R4rM9I+8M2rhmsuLwAMmkLQWE= golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d h1:2+ZP7EfsZV7Vvmx3TIqSlSzATMkTAKqM14YGFPoSKjI= +golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -95,12 +100,16 @@ golang.org/x/net v0.0.0-20181106065722-10aee1819953 h1:LuZIitY8waaxUfNIdtajyE/Yz golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190206173232-65e2d4e15006 h1:bfLnR+k0tq5Lqt6dflRLcZiz6UaXCMt3vhYJ1l4FQ80= golang.org/x/net v0.0.0-20190206173232-65e2d4e15006/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd h1:DBH9mDw0zluJT/R+nGuV3jWFWLFaHyYZWD4tOT+cjn0= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5 h1:LfCXLvNmTYH9kEmVgqbnsWfruoXZIrh4YBgqVHtDvw0= diff --git a/waddrmgr/manager.go b/waddrmgr/manager.go index 45ee067..e003c0f 100644 --- a/waddrmgr/manager.go +++ b/waddrmgr/manager.go @@ -1372,13 +1372,17 @@ func deriveCoinTypeKey(masterNode *hdkeychain.ExtendedKey, // The branch is 0 for external addresses and 1 for internal addresses. // Derive the purpose key as a child of the master node. - purpose, err := masterNode.Child(scope.Purpose + hdkeychain.HardenedKeyStart) + purpose, err := masterNode.DeriveNonStandard( + scope.Purpose + hdkeychain.HardenedKeyStart, + ) if err != nil { return nil, err } // Derive the coin type key as a child of the purpose key. - coinTypeKey, err := purpose.Child(scope.Coin + hdkeychain.HardenedKeyStart) + coinTypeKey, err := purpose.DeriveNonStandard( + scope.Coin + hdkeychain.HardenedKeyStart, + ) if err != nil { return nil, err } @@ -1401,7 +1405,9 @@ func deriveAccountKey(coinTypeKey *hdkeychain.ExtendedKey, } // Derive the account key as a child of the coin type key. - return coinTypeKey.Child(account + hdkeychain.HardenedKeyStart) + return coinTypeKey.DeriveNonStandard( + account + hdkeychain.HardenedKeyStart, + ) } // checkBranchKeys ensures deriving the extended keys for the internal and @@ -1416,12 +1422,12 @@ func deriveAccountKey(coinTypeKey *hdkeychain.ExtendedKey, // The branch is 0 for external addresses and 1 for internal addresses. func checkBranchKeys(acctKey *hdkeychain.ExtendedKey) error { // Derive the external branch as the first child of the account key. - if _, err := acctKey.Child(ExternalBranch); err != nil { + if _, err := acctKey.DeriveNonStandard(ExternalBranch); err != nil { return err } - // Derive the external branch as the second child of the account key. - _, err := acctKey.Child(InternalBranch) + // Derive the internal branch as the second child of the account key. + _, err := acctKey.DeriveNonStandard(InternalBranch) return err } diff --git a/waddrmgr/scoped_manager.go b/waddrmgr/scoped_manager.go index 44f9b95..f449a4e 100644 --- a/waddrmgr/scoped_manager.go +++ b/waddrmgr/scoped_manager.go @@ -271,14 +271,14 @@ func (s *ScopedKeyManager) deriveKey(acctInfo *accountInfo, branch, } // Derive and return the key. - branchKey, err := acctKey.Child(branch) + branchKey, err := acctKey.DeriveNonStandard(branch) if err != nil { str := fmt.Sprintf("failed to derive extended key branch %d", branch) return nil, managerError(ErrKeyChain, str, err) } - addressKey, err := branchKey.Child(index) + addressKey, err := branchKey.DeriveNonStandard(index) branchKey.Zero() // Zero branch key after it's used. if err != nil { str := fmt.Sprintf("failed to derive child extended key -- "+ @@ -715,7 +715,7 @@ func (s *ScopedKeyManager) nextAddresses(ns walletdb.ReadWriteBucket, } // Derive the appropriate branch key and ensure it is zeroed when done. - branchKey, err := acctKey.Child(branchNum) + branchKey, err := acctKey.DeriveNonStandard(branchNum) if err != nil { str := fmt.Sprintf("failed to derive extended key branch %d", branchNum) @@ -732,7 +732,7 @@ func (s *ScopedKeyManager) nextAddresses(ns walletdb.ReadWriteBucket, var nextKey *hdkeychain.ExtendedKey for { // Derive the next child in the external chain branch. - key, err := branchKey.Child(nextIndex) + key, err := branchKey.DeriveNonStandard(nextIndex) if err != nil { // When this particular child is invalid, skip to the // next index. @@ -915,7 +915,7 @@ func (s *ScopedKeyManager) extendAddresses(ns walletdb.ReadWriteBucket, } // Derive the appropriate branch key and ensure it is zeroed when done. - branchKey, err := acctKey.Child(branchNum) + branchKey, err := acctKey.DeriveNonStandard(branchNum) if err != nil { str := fmt.Sprintf("failed to derive extended key branch %d", branchNum) @@ -934,7 +934,7 @@ func (s *ScopedKeyManager) extendAddresses(ns walletdb.ReadWriteBucket, var nextKey *hdkeychain.ExtendedKey for { // Derive the next child in the external chain branch. - key, err := branchKey.Child(nextIndex) + key, err := branchKey.DeriveNonStandard(nextIndex) if err != nil { // When this particular child is invalid, skip to the // next index. From ee9a1fb0ce9610322cc665ad81ed6ac7919c6fb8 Mon Sep 17 00:00:00 2001 From: Wilmer Paulino Date: Tue, 16 Feb 2021 17:01:05 -0800 Subject: [PATCH 05/16] waddrmgr: refactor code from ImportPrivateKey into methods --- waddrmgr/scoped_manager.go | 115 +++++++++++++++++++++++++------------ 1 file changed, 78 insertions(+), 37 deletions(-) diff --git a/waddrmgr/scoped_manager.go b/waddrmgr/scoped_manager.go index f449a4e..08d8c1a 100644 --- a/waddrmgr/scoped_manager.go +++ b/waddrmgr/scoped_manager.go @@ -1513,14 +1513,56 @@ func (s *ScopedKeyManager) ImportPrivateKey(ns walletdb.ReadWriteBucket, return nil, managerError(ErrLocked, errLocked, nil) } + // Encrypt the private key when not a watching-only address manager. + var encryptedPrivKey []byte + if !s.rootManager.WatchOnly() { + privKeyBytes := wif.PrivKey.Serialize() + var err error + encryptedPrivKey, err = s.rootManager.cryptoKeyPriv.Encrypt(privKeyBytes) + zero.Bytes(privKeyBytes) + if err != nil { + str := fmt.Sprintf("failed to encrypt private key for %x", + wif.PrivKey.PubKey().SerializeCompressed()) + return nil, managerError(ErrCrypto, str, err) + } + } + + err := s.importPublicKey(ns, wif.SerializePubKey(), encryptedPrivKey, bs) + if err != nil { + return nil, err + } + + // The full derivation path for an imported key is incomplete as we + // don't know exactly how it was derived. + importedDerivationPath := DerivationPath{ + Account: ImportedAddrAccount, + } + + // Create a new managed address based on the imported address. + if !s.rootManager.WatchOnly() { + return s.toPrivateManagedAddress( + wif, true, importedDerivationPath, + ) + } + pubKey := (*btcec.PublicKey)(&wif.PrivKey.PublicKey) + return s.toPublicManagedAddress( + pubKey, wif.CompressPubKey, true, importedDerivationPath, + ) +} + +// importPublicKey imports a public key into the address manager and updates the +// wallet's start block if necessary. An error is returned if the public key +// already exists. +func (s *ScopedKeyManager) importPublicKey(ns walletdb.ReadWriteBucket, + serializedPubKey, encryptedPrivKey []byte, bs *BlockStamp) error { + // Prevent duplicates. - serializedPubKey := wif.SerializePubKey() pubKeyHash := btcutil.Hash160(serializedPubKey) alreadyExists := s.existsAddress(ns, pubKeyHash) if alreadyExists { str := fmt.Sprintf("address for public key %x already exists", serializedPubKey) - return nil, managerError(ErrDuplicateAddress, str, nil) + return managerError(ErrDuplicateAddress, str, nil) } // Encrypt public key. @@ -1530,20 +1572,7 @@ func (s *ScopedKeyManager) ImportPrivateKey(ns walletdb.ReadWriteBucket, if err != nil { str := fmt.Sprintf("failed to encrypt public key for %x", serializedPubKey) - return nil, managerError(ErrCrypto, str, err) - } - - // Encrypt the private key when not a watching-only address manager. - var encryptedPrivKey []byte - if !s.rootManager.WatchOnly() { - privKeyBytes := wif.PrivKey.Serialize() - encryptedPrivKey, err = s.rootManager.cryptoKeyPriv.Encrypt(privKeyBytes) - zero.Bytes(privKeyBytes) - if err != nil { - str := fmt.Sprintf("failed to encrypt private key for %x", - serializedPubKey) - return nil, managerError(ErrCrypto, str, err) - } + return managerError(ErrCrypto, str, err) } // The start block needs to be updated when the newly imported address @@ -1559,13 +1588,13 @@ func (s *ScopedKeyManager) ImportPrivateKey(ns walletdb.ReadWriteBucket, encryptedPubKey, encryptedPrivKey, ) if err != nil { - return nil, err + return err } if updateStartBlock { err := putStartBlock(ns, bs) if err != nil { - return nil, err + return err } } @@ -1577,30 +1606,42 @@ func (s *ScopedKeyManager) ImportPrivateKey(ns walletdb.ReadWriteBucket, s.rootManager.mtx.Unlock() } - // The full derivation path for an imported key is incomplete as we - // don't know exactly how it was derived. - importedDerivationPath := DerivationPath{ - Account: ImportedAddrAccount, - } + return nil +} + +// toPrivateManagedAddress converts a private key to a managed address. +func (s *ScopedKeyManager) toPrivateManagedAddress(wif *btcutil.WIF, + imported bool, derivationPath DerivationPath) (*managedAddress, error) { // Create a new managed address based on the imported address. - var managedAddr *managedAddress - if !s.rootManager.WatchOnly() { - managedAddr, err = newManagedAddress( - s, importedDerivationPath, wif.PrivKey, - wif.CompressPubKey, s.addrSchema.ExternalAddrType, - ) - } else { - pubKey := (*btcec.PublicKey)(&wif.PrivKey.PublicKey) - managedAddr, err = newManagedAddressWithoutPrivKey( - s, importedDerivationPath, pubKey, wif.CompressPubKey, - s.addrSchema.ExternalAddrType, - ) - } + managedAddr, err := newManagedAddress( + s, derivationPath, wif.PrivKey, wif.CompressPubKey, + s.addrSchema.ExternalAddrType, + ) if err != nil { return nil, err } - managedAddr.imported = true + managedAddr.imported = imported + + // Add the new managed address to the cache of recent addresses and + // return it. + s.addrs[addrKey(managedAddr.Address().ScriptAddress())] = managedAddr + return managedAddr, nil +} + +// toPublicManagedAddress converts a public key to a managed address. +func (s *ScopedKeyManager) toPublicManagedAddress(pubKey *btcec.PublicKey, + compressed, imported bool, derivationPath DerivationPath) (*managedAddress, error) { + + // Create a new managed address based on the imported address. + managedAddr, err := newManagedAddressWithoutPrivKey( + s, derivationPath, pubKey, compressed, + s.addrSchema.ExternalAddrType, + ) + if err != nil { + return nil, err + } + managedAddr.imported = imported // Add the new managed address to the cache of recent addresses and // return it. From dead1a89d9511c90c23c73a12d9fd187697f03b2 Mon Sep 17 00:00:00 2001 From: Wilmer Paulino Date: Tue, 16 Feb 2021 17:01:08 -0800 Subject: [PATCH 06/16] waddrmgr: add ImportPublicKey --- waddrmgr/scoped_manager.go | 66 +++++++++++++++++++++++++++++++++++--- 1 file changed, 61 insertions(+), 5 deletions(-) diff --git a/waddrmgr/scoped_manager.go b/waddrmgr/scoped_manager.go index 08d8c1a..05daf4f 100644 --- a/waddrmgr/scoped_manager.go +++ b/waddrmgr/scoped_manager.go @@ -6,6 +6,7 @@ import ( "github.com/btcsuite/btcd/btcec" "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcutil" "github.com/btcsuite/btcutil/hdkeychain" "github.com/btcsuite/btcwallet/internal/zero" @@ -1527,7 +1528,10 @@ func (s *ScopedKeyManager) ImportPrivateKey(ns walletdb.ReadWriteBucket, } } - err := s.importPublicKey(ns, wif.SerializePubKey(), encryptedPrivKey, bs) + err := s.importPublicKey( + ns, wif.SerializePubKey(), encryptedPrivKey, + s.addrSchema.ExternalAddrType, bs, + ) if err != nil { return nil, err } @@ -1550,15 +1554,67 @@ func (s *ScopedKeyManager) ImportPrivateKey(ns walletdb.ReadWriteBucket, ) } +// ImportPublicKey imports a public key into the address manager. +// +// All imported addresses will be part of the account defined by the +// ImportedAddrAccount constant. +func (s *ScopedKeyManager) ImportPublicKey(ns walletdb.ReadWriteBucket, + pubKey *btcec.PublicKey, bs *BlockStamp) (ManagedAddress, error) { + + s.mtx.Lock() + defer s.mtx.Unlock() + + serializedPubKey := pubKey.SerializeCompressed() + err := s.importPublicKey( + ns, serializedPubKey, nil, s.addrSchema.ExternalAddrType, bs, + ) + if err != nil { + return nil, err + } + + // The full derivation path for an imported key is incomplete as we + // don't know exactly how it was derived. + importedDerivationPath := DerivationPath{ + Account: ImportedAddrAccount, + } + return s.toPublicManagedAddress( + pubKey, true, true, importedDerivationPath, + ) +} + // importPublicKey imports a public key into the address manager and updates the // wallet's start block if necessary. An error is returned if the public key // already exists. func (s *ScopedKeyManager) importPublicKey(ns walletdb.ReadWriteBucket, - serializedPubKey, encryptedPrivKey []byte, bs *BlockStamp) error { + serializedPubKey, encryptedPrivKey []byte, addrType AddressType, + bs *BlockStamp) error { + + // Compute the addressID for our key based on its address type. + var addressID []byte + switch addrType { + case PubKeyHash, WitnessPubKey: + addressID = btcutil.Hash160(serializedPubKey) + + case NestedWitnessPubKey: + pubKeyHash := btcutil.Hash160(serializedPubKey) + p2wkhAddr, err := btcutil.NewAddressWitnessPubKeyHash( + pubKeyHash, s.rootManager.chainParams, + ) + if err != nil { + return err + } + witnessScript, err := txscript.PayToAddrScript(p2wkhAddr) + if err != nil { + return err + } + addressID = btcutil.Hash160(witnessScript) + + default: + return fmt.Errorf("unsupported address type %v", addrType) + } // Prevent duplicates. - pubKeyHash := btcutil.Hash160(serializedPubKey) - alreadyExists := s.existsAddress(ns, pubKeyHash) + alreadyExists := s.existsAddress(ns, addressID) if alreadyExists { str := fmt.Sprintf("address for public key %x already exists", serializedPubKey) @@ -1584,7 +1640,7 @@ func (s *ScopedKeyManager) importPublicKey(ns walletdb.ReadWriteBucket, // Save the new imported address to the db and update start block (if // needed) in a single transaction. err = putImportedAddress( - ns, &s.scope, pubKeyHash, ImportedAddrAccount, ssNone, + ns, &s.scope, addressID, ImportedAddrAccount, ssNone, encryptedPubKey, encryptedPrivKey, ) if err != nil { From 0492cb45070963f5fa167106571f7f9959b564c1 Mon Sep 17 00:00:00 2001 From: Wilmer Paulino Date: Tue, 16 Feb 2021 17:01:12 -0800 Subject: [PATCH 07/16] 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. --- rpc/legacyrpc/methods.go | 4 +- waddrmgr/address.go | 19 +++--- waddrmgr/common_test.go | 71 +++++++++++--------- waddrmgr/manager.go | 6 +- waddrmgr/manager_test.go | 131 +++++++++++++++++++----------------- waddrmgr/scoped_manager.go | 133 ++++++++++++++++++++++++------------- wallet/notifications.go | 2 +- wallet/wallet.go | 14 ++-- 8 files changed, 221 insertions(+), 159 deletions(-) diff --git a/rpc/legacyrpc/methods.go b/rpc/legacyrpc/methods.go index 7fe9add..bd942a9 100644 --- a/rpc/legacyrpc/methods.go +++ b/rpc/legacyrpc/methods.go @@ -1774,7 +1774,9 @@ func validateAddress(icmd interface{}, w *wallet.Wallet) (interface{}, error) { // The address lookup was successful which means there is further // information about it available and it is "mine". result.IsMine = true - acctName, err := w.AccountName(waddrmgr.KeyScopeBIP0044, ainfo.Account()) + acctName, err := w.AccountName( + waddrmgr.KeyScopeBIP0044, ainfo.InternalAccount(), + ) if err != nil { return nil, &ErrAccountNameNotFound } diff --git a/waddrmgr/address.go b/waddrmgr/address.go index b8008ae..49c6a26 100644 --- a/waddrmgr/address.go +++ b/waddrmgr/address.go @@ -54,8 +54,8 @@ const ( // type may provide further fields to provide information specific to that type // of address. type ManagedAddress interface { - // Account returns the account the address is associated with. - Account() uint32 + // Account returns the internal account the address is associated with. + InternalAccount() uint32 // Address returns a btcutil.Address for the backing address. Address() btcutil.Address @@ -133,7 +133,7 @@ type managedAddress struct { used bool addrType AddressType pubKey *btcec.PublicKey - privKeyEncrypted []byte + privKeyEncrypted []byte // nil if part of watch-only account privKeyCT []byte // non-nil if unlocked privKeyMutex sync.Mutex } @@ -177,11 +177,12 @@ func (a *managedAddress) lock() { 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. -func (a *managedAddress) Account() uint32 { - return a.derivationPath.Account +func (a *managedAddress) InternalAccount() uint32 { + return a.derivationPath.InternalAccount } // AddrType returns the address type of the managed address. This can be used @@ -544,11 +545,11 @@ func (a *scriptAddress) lock() { a.scriptMutex.Unlock() } -// Account returns the account the address is associated with. This will always -// be the ImportedAddrAccount constant for script addresses. +// InternalAccount returns the account the address is associated with. This will +// always be the ImportedAddrAccount constant for script addresses. // // This is part of the ManagedAddress interface implementation. -func (a *scriptAddress) Account() uint32 { +func (a *scriptAddress) InternalAccount() uint32 { return a.account } diff --git a/waddrmgr/common_test.go b/waddrmgr/common_test.go index d14301e..cd4eff9 100644 --- a/waddrmgr/common_test.go +++ b/waddrmgr/common_test.go @@ -13,6 +13,7 @@ import ( "time" "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcutil/hdkeychain" "github.com/btcsuite/btcwallet/walletdb" _ "github.com/btcsuite/btcwallet/walletdb/bdb" ) @@ -51,9 +52,10 @@ var ( privKey: hexToBytes("c27d6581b92785834b381fa697c4b0ffc4574b495743722e0acb7601b1b68b99"), privKeyWIF: "L3jmpy54Pc7MLXTN2mL8Xas7BJziwKaUGmgnXXzgGbVRdiAniXZk", derivationInfo: DerivationPath{ - Account: 0, - Branch: 0, - Index: 0, + InternalAccount: 0, + Account: hdkeychain.HardenedKeyStart, + Branch: 0, + Index: 0, }, }, { @@ -66,9 +68,10 @@ var ( privKey: hexToBytes("18f3b191019e83878a81557abebb2afda199e31d22e150d8bf4df4561671be6c"), privKeyWIF: "Kx4DNid19W8sjNFN3uPqQE7UYnCqyEp7unCvdkf2LrVUFpnDtwpB", derivationInfo: DerivationPath{ - Account: 0, - Branch: 0, - Index: 1, + InternalAccount: 0, + Account: hdkeychain.HardenedKeyStart, + Branch: 0, + Index: 1, }, }, { @@ -81,9 +84,10 @@ var ( privKey: hexToBytes("ccb8f6305b73136b363644b647f6efc0fd27b6b7d9c11c7e560662ed38db7b34"), privKeyWIF: "L45fWF6Yd736fDohuB97vwRRLdQQJr3ZGvbokk9ubiT7aNrg7tTn", derivationInfo: DerivationPath{ - Account: 0, - Branch: 0, - Index: 2, + InternalAccount: 0, + Account: hdkeychain.HardenedKeyStart, + Branch: 0, + Index: 2, }, }, { @@ -96,9 +100,10 @@ var ( privKey: hexToBytes("d6bc8ff768814fede2adcdb74826bd846924341b3862e3b6e31cdc084e992940"), privKeyWIF: "L4R8XyxYQyPSpTwj8w96tM86a6j3QA9jbRPj3RA7DVTVWk71ndeP", derivationInfo: DerivationPath{ - Account: 0, - Branch: 0, - Index: 3, + InternalAccount: 0, + Account: hdkeychain.HardenedKeyStart, + Branch: 0, + Index: 3, }, }, { @@ -111,9 +116,10 @@ var ( privKey: hexToBytes("8563ade061110e03aee50695ffc5cb1c06c8310bde0a3674257c853c966968c0"), privKeyWIF: "L1h16Hunxomww4FrpyQP2iFmWNgG7U1u3awp6Vd3s2uGf7v5VU8c", derivationInfo: DerivationPath{ - Account: 0, - Branch: 0, - Index: 4, + InternalAccount: 0, + Account: hdkeychain.HardenedKeyStart, + Branch: 0, + Index: 4, }, }, { @@ -126,9 +132,10 @@ var ( privKey: hexToBytes("fe4f855fcf059ec6ddf7b25f63b19aa49c771d1fcb9850b68ae3d65e20657a60"), privKeyWIF: "L5k4HivqXvohxBMpuwD38iUgi6uewffwZny91ZNYfM39RXH2x3QR", derivationInfo: DerivationPath{ - Account: 0, - Branch: 1, - Index: 0, + InternalAccount: 0, + Account: hdkeychain.HardenedKeyStart, + Branch: 1, + Index: 0, }, }, { @@ -141,9 +148,10 @@ var ( privKey: hexToBytes("bfef521317c65b018ae7e6d7ecc3aa700d5d0f7ea84d567be9270382d0b5e3e6"), privKeyWIF: "L3eomUajnTDM3Pc8GU47qqXUFuCjvpqY7NYN9mH3x1ZFjDgiY4BU", derivationInfo: DerivationPath{ - Account: 0, - Branch: 1, - Index: 1, + InternalAccount: 0, + Account: hdkeychain.HardenedKeyStart, + Branch: 1, + Index: 1, }, }, { @@ -156,9 +164,10 @@ var ( privKey: hexToBytes("f506dffd4494c24006df7a35f3291f7ca0297a1a431557a1339bfed6f48738ca"), privKeyWIF: "L5S1bVQUPqQb1Su82fLoSpnGCjcPfdAQE1pJxWRopJSBdYNDHESv", derivationInfo: DerivationPath{ - Account: 0, - Branch: 1, - Index: 2, + InternalAccount: 0, + Account: hdkeychain.HardenedKeyStart, + Branch: 1, + Index: 2, }, }, { @@ -171,9 +180,10 @@ var ( privKey: hexToBytes("b3629de8ef6a275b4ffae41aa2bbbc2952eb92282ea6402435abbb010ecc1fb8"), privKeyWIF: "L3EQsGeEnyXmKaux54cG4DQeCSQDvGuvEuy3W2ss4geum7AtWaHw", derivationInfo: DerivationPath{ - Account: 0, - Branch: 1, - Index: 3, + InternalAccount: 0, + Account: hdkeychain.HardenedKeyStart, + Branch: 1, + Index: 3, }, }, { @@ -186,9 +196,10 @@ var ( privKey: hexToBytes("ca747a7ef815ea0dbe68655272cecbfbd65f2a109019a9ed28e0d3dcaffe05c3"), privKeyWIF: "L41Frac75RPbTELKzw1EGC2qCkdveiVumpmsyX4daAvyyCMxit1W", derivationInfo: DerivationPath{ - Account: 0, - Branch: 1, - Index: 4, + InternalAccount: 0, + Account: hdkeychain.HardenedKeyStart, + Branch: 1, + Index: 4, }, }, } diff --git a/waddrmgr/manager.go b/waddrmgr/manager.go index e003c0f..e3aa97f 100644 --- a/waddrmgr/manager.go +++ b/waddrmgr/manager.go @@ -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 // them being created while the address manager was locked. for _, info := range manager.deriveOnUnlock { - addressKey, err := manager.deriveKeyFromPath( - ns, info.managedAddr.Account(), info.branch, - info.index, true, + addressKey, _, err := manager.deriveKeyFromPath( + ns, info.managedAddr.InternalAccount(), + info.branch, info.index, true, ) if err != nil { m.lock() diff --git a/waddrmgr/manager_test.go b/waddrmgr/manager_test.go index c4667ad..dd2139f 100644 --- a/waddrmgr/manager_test.go +++ b/waddrmgr/manager_test.go @@ -71,15 +71,15 @@ func failingSecretKeyGen(passphrase *[]byte, // blocks have been inserted and therefore some of the transaction outputs are // spent. type testContext struct { - t *testing.T - caseName string - db walletdb.DB - rootManager *Manager - manager *ScopedKeyManager - account uint32 - create bool - unlocked bool - watchingOnly bool + t *testing.T + caseName string + db walletdb.DB + rootManager *Manager + manager *ScopedKeyManager + internalAccount uint32 + create bool + unlocked bool + watchingOnly bool } // addrType is the type of address being tested @@ -114,7 +114,7 @@ func testNamePrefix(tc *testContext) string { 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 @@ -293,9 +293,9 @@ func testManagedScriptAddress(tc *testContext, prefix string, func testAddress(tc *testContext, prefix string, gotAddr ManagedAddress, wantAddr *expectedAddr) bool { - if gotAddr.Account() != tc.account { + if gotAddr.InternalAccount() != tc.internalAccount { tc.t.Errorf("ManagedAddress.Account: unexpected account - got "+ - "%d, want %d", gotAddr.Account(), tc.account) + "%d, want %d", gotAddr.InternalAccount(), tc.internalAccount) return false } @@ -360,7 +360,9 @@ func testExternalAddresses(tc *testContext) bool { err := walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) error { ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) var err error - addrs, err = tc.manager.NextExternalAddresses(ns, tc.account, 5) + addrs, err = tc.manager.NextExternalAddresses( + ns, tc.internalAccount, 5, + ) return err }) if err != nil { @@ -395,7 +397,9 @@ func testExternalAddresses(tc *testContext) bool { err := walletdb.View(tc.db, func(tx walletdb.ReadTx) error { ns := tx.ReadBucket(waddrmgrNamespaceKey) var err error - lastAddr, err = tc.manager.LastExternalAddress(ns, tc.account) + lastAddr, err = tc.manager.LastExternalAddress( + ns, tc.internalAccount, + ) return err }) if err != nil { @@ -512,7 +516,9 @@ func testInternalAddresses(tc *testContext) bool { err := walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) error { ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) var err error - addrs, err = tc.manager.NextInternalAddresses(ns, tc.account, 5) + addrs, err = tc.manager.NextInternalAddresses( + ns, tc.internalAccount, 5, + ) return err }) if err != nil { @@ -547,7 +553,9 @@ func testInternalAddresses(tc *testContext) bool { err := walletdb.View(tc.db, func(tx walletdb.ReadTx) error { ns := tx.ReadBucket(waddrmgrNamespaceKey) var err error - lastAddr, err = tc.manager.LastInternalAddress(ns, tc.account) + lastAddr, err = tc.manager.LastInternalAddress( + ns, tc.internalAccount, + ) return err }) if err != nil { @@ -776,7 +784,7 @@ func testImportPrivateKey(tc *testContext) bool { } // Only import the private keys when in the create phase of testing. - tc.account = ImportedAddrAccount + tc.internalAccount = ImportedAddrAccount prefix := testNamePrefix(tc) + " testImportPrivateKey" if tc.create { 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. - tc.account = ImportedAddrAccount + tc.internalAccount = ImportedAddrAccount prefix := testNamePrefix(tc) if tc.create { for i, test := range tests { @@ -1320,7 +1328,7 @@ func testNewAccount(tc *testContext) bool { tc.unlocked = true testName := "acct-create" - expectedAccount := tc.account + 1 + expectedAccount := tc.internalAccount + 1 if !tc.create { // Create a new account in open mode testName = "acct-open" @@ -1480,7 +1488,7 @@ func testRenameAccount(tc *testContext) bool { err := walletdb.View(tc.db, func(tx walletdb.ReadTx) error { ns := tx.ReadBucket(waddrmgrNamespaceKey) var err error - acctName, err = tc.manager.AccountName(ns, tc.account) + acctName, err = tc.manager.AccountName(ns, tc.internalAccount) return err }) if err != nil { @@ -1490,7 +1498,7 @@ func testRenameAccount(tc *testContext) bool { testName := acctName + "-renamed" err = walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) error { ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) - return tc.manager.RenameAccount(ns, tc.account, testName) + return tc.manager.RenameAccount(ns, tc.internalAccount, testName) }) if err != nil { 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 { ns := tx.ReadBucket(waddrmgrNamespaceKey) var err error - newName, err = tc.manager.AccountName(ns, tc.account) + newName, err = tc.manager.AccountName(ns, tc.internalAccount) return err }) if err != nil { @@ -1516,7 +1524,7 @@ func testRenameAccount(tc *testContext) bool { // Test duplicate account name error err = walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) error { ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) - return tc.manager.RenameAccount(ns, tc.account, testName) + return tc.manager.RenameAccount(ns, tc.internalAccount, testName) }) wantErrCode := ErrDuplicateAccount if !checkManagerError(tc.t, testName, err, wantErrCode) { @@ -1587,7 +1595,7 @@ func testForEachAccountAddress(tc *testContext) bool { var addrs []ManagedAddress err := walletdb.View(tc.db, func(tx walletdb.ReadTx) error { ns := tx.ReadBucket(waddrmgrNamespaceKey) - return tc.manager.ForEachAccountAddress(ns, tc.account, + return tc.manager.ForEachAccountAddress(ns, tc.internalAccount, func(maddr ManagedAddress) error { addrs = append(addrs, maddr) return nil @@ -1632,14 +1640,14 @@ func testManagerAPI(tc *testContext, caseCreatedWatchingOnly bool) { testChangePassphrase(tc) // Reset default account - tc.account = 0 + tc.internalAccount = 0 testNewAccount(tc) testLookupAccount(tc) testForEachAccount(tc) testForEachAccountAddress(tc) // Rename account 1 "acct-create" - tc.account = 1 + tc.internalAccount = 1 testRenameAccount(tc) } else { // Test API for created watch-only case. @@ -1720,14 +1728,14 @@ func testConvertWatchingOnly(tc *testContext) bool { return false } testManagerAPI(&testContext{ - t: tc.t, - caseName: tc.caseName, - db: db, - rootManager: mgr, - manager: scopedMgr, - account: 0, - create: false, - watchingOnly: true, + t: tc.t, + caseName: tc.caseName, + db: db, + rootManager: mgr, + manager: scopedMgr, + internalAccount: 0, + create: false, + watchingOnly: true, }, false) mgr.Close() @@ -1751,14 +1759,14 @@ func testConvertWatchingOnly(tc *testContext) bool { } testManagerAPI(&testContext{ - t: tc.t, - caseName: tc.caseName, - db: db, - rootManager: mgr, - manager: scopedMgr, - account: 0, - create: false, - watchingOnly: true, + t: tc.t, + caseName: tc.caseName, + db: db, + rootManager: mgr, + manager: scopedMgr, + internalAccount: 0, + create: false, + watchingOnly: true, }, false) 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 // manager after they've completed testManagerAPI(&testContext{ - t: t, - caseName: caseName, - db: db, - manager: scopedMgr, - rootManager: mgr, - account: 0, - create: true, - watchingOnly: caseCreatedWatchingOnly, + t: t, + caseName: caseName, + db: db, + manager: scopedMgr, + rootManager: mgr, + internalAccount: 0, + create: true, + watchingOnly: caseCreatedWatchingOnly, }, caseCreatedWatchingOnly) mgr.Close() @@ -1981,14 +1989,14 @@ func testManagerCase(t *testing.T, caseName string, t.Fatalf("(%s) unable to fetch default scope: %v", caseName, err) } tc := &testContext{ - t: t, - caseName: caseName, - db: db, - manager: scopedMgr, - rootManager: mgr, - account: 0, - create: false, - watchingOnly: caseCreatedWatchingOnly, + t: t, + caseName: caseName, + db: db, + manager: scopedMgr, + rootManager: mgr, + internalAccount: 0, + create: false, + watchingOnly: caseCreatedWatchingOnly, } testManagerAPI(tc, caseCreatedWatchingOnly) @@ -2743,9 +2751,10 @@ func testNewRawAccount(t *testing.T, mgr *Manager, db walletdb.DB, ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) keyPath := DerivationPath{ - Account: accountNum, - Branch: 0, - Index: 0, + InternalAccount: accountNum, + Account: hdkeychain.HardenedKeyStart, + Branch: 0, + Index: 0, } accountTargetAddr, err = scopedMgr.DeriveFromKeyPath( ns, keyPath, diff --git a/waddrmgr/scoped_manager.go b/waddrmgr/scoped_manager.go index 05daf4f..4ffe192 100644 --- a/waddrmgr/scoped_manager.go +++ b/waddrmgr/scoped_manager.go @@ -23,6 +23,10 @@ import ( // m/purpose'/cointype'/account/branch/index, where purpose' and cointype' are // bound by the scope of a particular manager. 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 // manager's hardened coin type key. Account uint32 @@ -213,21 +217,15 @@ func (s *ScopedKeyManager) Close() { // // This function MUST be called with the manager lock held for writes. func (s *ScopedKeyManager) keyToManaged(derivedKey *hdkeychain.ExtendedKey, - account, branch, index uint32) (ManagedAddress, error) { + derivationPath DerivationPath) (ManagedAddress, error) { var addrType AddressType - if branch == InternalBranch { + if derivationPath.Branch == InternalBranch { addrType = s.addrSchema.InternalAddrType } else { addrType = s.addrSchema.ExternalAddrType } - derivationPath := DerivationPath{ - Account: account, - Branch: branch, - Index: index, - } - // Create a new managed address based on the public or private key // depending on whether the passed key is private. Also, zero the key // after creating the managed address from it. @@ -245,13 +243,13 @@ func (s *ScopedKeyManager) keyToManaged(derivedKey *hdkeychain.ExtendedKey, // unlocked. info := unlockDeriveInfo{ managedAddr: ma, - branch: branch, - index: index, + branch: derivationPath.Branch, + index: derivationPath.Index, } s.deriveOnUnlock = append(s.deriveOnUnlock, &info) } - if branch == InternalBranch { + if derivationPath.Branch == InternalBranch { ma.internal = true } @@ -343,7 +341,9 @@ func (s *ScopedKeyManager) loadAccountInfo(ns walletdb.ReadBucket, 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 // extended keys. decrypted, err := s.rootManager.cryptoKeyPriv.Decrypt(acctInfo.acctKeyEncrypted) @@ -367,13 +367,17 @@ func (s *ScopedKeyManager) loadAccountInfo(ns walletdb.ReadBucket, if index > 0 { index-- } - lastExtKey, err := s.deriveKey( - acctInfo, branch, index, !s.rootManager.isLocked(), - ) + lastExtAddrPath := DerivationPath{ + InternalAccount: account, + Account: acctKeyPub.ChildIndex(), + Branch: branch, + Index: index, + } + lastExtKey, err := s.deriveKey(acctInfo, branch, index, private) if err != nil { return nil, err } - lastExtAddr, err := s.keyToManaged(lastExtKey, account, branch, index) + lastExtAddr, err := s.keyToManaged(lastExtKey, lastExtAddrPath) if err != nil { return nil, err } @@ -384,13 +388,17 @@ func (s *ScopedKeyManager) loadAccountInfo(ns walletdb.ReadBucket, if index > 0 { index-- } - lastIntKey, err := s.deriveKey( - acctInfo, branch, index, !s.rootManager.isLocked(), - ) + lastIntAddrPath := DerivationPath{ + InternalAccount: account, + Account: acctKeyPub.ChildIndex(), + Branch: branch, + Index: index, + } + lastIntKey, err := s.deriveKey(acctInfo, branch, index, private) if err != nil { return nil, err } - lastIntAddr, err := s.keyToManaged(lastIntKey, account, branch, index) + lastIntAddr, err := s.keyToManaged(lastIntKey, lastIntAddrPath) if err != nil { 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 // scheme) from a given key path. If key derivation isn't possible, then an // 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, kp DerivationPath) (ManagedAddress, error) { s.mtx.Lock() defer s.mtx.Unlock() - extKey, err := s.deriveKeyFromPath( - ns, kp.Account, kp.Branch, kp.Index, !s.rootManager.IsLocked(), + watchOnly := s.rootManager.WatchOnly() + private := !s.rootManager.IsLocked() && !watchOnly + + addrKey, _, err := s.deriveKeyFromPath( + ns, kp.InternalAccount, kp.Branch, kp.Index, private, ) if err != nil { 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 -// 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. -func (s *ScopedKeyManager) deriveKeyFromPath(ns walletdb.ReadBucket, account, branch, - index uint32, private bool) (*hdkeychain.ExtendedKey, error) { +func (s *ScopedKeyManager) deriveKeyFromPath(ns walletdb.ReadBucket, + internalAccount, branch, index uint32, private bool) ( + *hdkeychain.ExtendedKey, *hdkeychain.ExtendedKey, error) { // Look up the account key information. - acctInfo, err := s.loadAccountInfo(ns, account) + acctInfo, err := s.loadAccountInfo(ns, internalAccount) 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 @@ -493,16 +520,22 @@ func (s *ScopedKeyManager) chainAddressRowToManaged(ns walletdb.ReadBucket, // Since the manger's mutex is assumed to held when invoking this // function, we use the internal isLocked to avoid a deadlock. - isLocked := s.rootManager.isLocked() + private := !s.rootManager.isLocked() && !s.rootManager.watchOnly() - addressKey, err := s.deriveKeyFromPath( - ns, row.account, row.branch, row.index, !isLocked, + addressKey, acctKey, err := s.deriveKeyFromPath( + ns, row.account, row.branch, row.index, private, ) if err != nil { return nil, err } - - return s.keyToManaged(addressKey, row.account, row.branch, row.index) + return s.keyToManaged( + addressKey, DerivationPath{ + InternalAccount: row.account, + Account: acctKey.ChildIndex(), + Branch: row.branch, + Index: row.index, + }, + ) } // 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 // derivation path, as we don't have enough information to do so. derivationPath := DerivationPath{ - Account: row.account, + InternalAccount: row.account, } 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 // is locked. acctKey := acctInfo.acctKeyPub - if !s.rootManager.IsLocked() { + watchOnly := s.rootManager.WatchOnly() || len(acctInfo.acctKeyEncrypted) == 0 + if !s.rootManager.IsLocked() && !watchOnly { acctKey = acctInfo.acctKeyPriv } @@ -757,9 +791,10 @@ func (s *ScopedKeyManager) nextAddresses(ns walletdb.ReadWriteBucket, // proper derivation path so this information can be available // to callers. derivationPath := DerivationPath{ - Account: account, - Branch: branchNum, - Index: nextIndex - 1, + InternalAccount: account, + Account: acctKey.ChildIndex(), + Branch: branchNum, + Index: nextIndex - 1, } // 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 // that need their private keys derived when the // address manager is next unlocked. - if s.rootManager.isLocked() && !s.rootManager.watchOnly() { + if s.rootManager.isLocked() && !watchOnly { 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 // is locked. acctKey := acctInfo.acctKeyPub - if !s.rootManager.IsLocked() { + watchOnly := s.rootManager.WatchOnly() || acctInfo.acctKeyPriv != nil + if !s.rootManager.IsLocked() && !watchOnly { acctKey = acctInfo.acctKeyPriv } @@ -959,9 +995,10 @@ func (s *ScopedKeyManager) extendAddresses(ns walletdb.ReadWriteBucket, // proper derivation path so this information can be available // to callers. derivationPath := DerivationPath{ - Account: account, - Branch: branchNum, - Index: nextIndex - 1, + InternalAccount: account, + Account: acctInfo.acctKeyPub.ChildIndex(), + Branch: branchNum, + Index: nextIndex - 1, } // 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 // need their private keys derived when the address manager is // next unlocked. - if s.rootManager.IsLocked() && !s.rootManager.WatchOnly() { + if s.rootManager.IsLocked() && !watchOnly { 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 // don't know exactly how it was derived. importedDerivationPath := DerivationPath{ - Account: ImportedAddrAccount, + InternalAccount: ImportedAddrAccount, } // 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 // don't know exactly how it was derived. importedDerivationPath := DerivationPath{ - Account: ImportedAddrAccount, + InternalAccount: ImportedAddrAccount, } return s.toPublicManagedAddress( pubKey, true, true, importedDerivationPath, @@ -1724,7 +1761,7 @@ func (s *ScopedKeyManager) ImportScript(ns walletdb.ReadWriteBucket, defer s.mtx.Unlock() // 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) } diff --git a/wallet/notifications.go b/wallet/notifications.go index d815576..fd943fa 100644 --- a/wallet/notifications.go +++ b/wallet/notifications.go @@ -90,7 +90,7 @@ func lookupOutputChain(dbtx walletdb.ReadTx, w *Wallet, details *wtxmgr.TxDetail if err != nil { log.Errorf("Cannot fetch account for wallet output: %v", err) } else { - account = ma.Account() + account = ma.InternalAccount() internal = ma.Internal() } return diff --git a/wallet/wallet.go b/wallet/wallet.go index e4d7172..7291de5 100644 --- a/wallet/wallet.go +++ b/wallet/wallet.go @@ -946,18 +946,20 @@ func expandScopeHorizons(ns walletdb.ReadWriteBucket, // externalKeyPath returns the relative external derivation path /0/0/index. func externalKeyPath(index uint32) waddrmgr.DerivationPath { return waddrmgr.DerivationPath{ - Account: waddrmgr.DefaultAccountNum, - Branch: waddrmgr.ExternalBranch, - Index: index, + InternalAccount: waddrmgr.DefaultAccountNum, + Account: waddrmgr.DefaultAccountNum, + Branch: waddrmgr.ExternalBranch, + Index: index, } } // internalKeyPath returns the relative internal derivation path /0/1/index. func internalKeyPath(index uint32) waddrmgr.DerivationPath { return waddrmgr.DerivationPath{ - Account: waddrmgr.DefaultAccountNum, - Branch: waddrmgr.InternalBranch, - Index: index, + InternalAccount: waddrmgr.DefaultAccountNum, + Account: waddrmgr.DefaultAccountNum, + Branch: waddrmgr.InternalBranch, + Index: index, } } From 198b0b8daeb60730647e28958b04dd8387575552 Mon Sep 17 00:00:00 2001 From: Wilmer Paulino Date: Tue, 16 Feb 2021 17:01:15 -0800 Subject: [PATCH 08/16] waddrmgr: store watch-only accounts under new account type Watch-only accounts are usually backed by an external signer as they do not contain any private key information. Some external signers require a root key fingerprint for identification and signing purposes. In order to guarantee compatibility with external signers, we need to persist the root key fingerprint within the database. Before this change, watch-only accounts used the default account database structure. In this commit, we introduce a new account type to store different information for watch-only accounts only. This isn't a breaking change as watch-only accounts have yet to be supported by the primary user of the wallet (lnd). With this new account type, we can avoid the empty private key fields, which are irrelevant to watch-only accounts, and we can store the root key fingerprint. --- waddrmgr/db.go | 289 +++++++++++++++++++++++++++++++++---- waddrmgr/manager.go | 4 +- waddrmgr/scoped_manager.go | 183 ++++++++++++++--------- 3 files changed, 377 insertions(+), 99 deletions(-) diff --git a/waddrmgr/db.go b/waddrmgr/db.go index 19167ea..1d8f6cf 100644 --- a/waddrmgr/db.go +++ b/waddrmgr/db.go @@ -6,6 +6,7 @@ package waddrmgr import ( + "bytes" "crypto/sha256" "encoding/binary" "errors" @@ -83,6 +84,12 @@ const ( // database. This is an account that re-uses the key derivation schema // of BIP0044-like accounts. accountDefault accountType = 0 // not iota as they need to be stable + + // accountWatchOnly is the account type used for storing watch-only + // accounts within the database. This is an account that re-uses the key + // derivation schema of BIP0044-like accounts and does not store private + // keys. + accountWatchOnly accountType = 1 ) // dbAccountRow houses information stored about an account in the database. @@ -102,6 +109,18 @@ type dbDefaultAccountRow struct { name string } +// dbWatchOnlyAccountRow houses additional information stored about a watch-only +// account in the databse. +type dbWatchOnlyAccountRow struct { + dbAccountRow + pubKeyEncrypted []byte + masterKeyFingerprint uint32 + nextExternalIndex uint32 + nextInternalIndex uint32 + name string + addrSchema *ScopeAddrSchema +} + // dbAddressRow houses common information stored about an address in the // database. type dbAddressRow struct { @@ -809,6 +828,159 @@ func serializeDefaultAccountRow(encryptedPubKey, encryptedPrivKey []byte, return rawData } +// deserializeWatchOnlyAccountRow deserializes the raw data from the passed +// account row as a watch-only account. +func deserializeWatchOnlyAccountRow(accountID []byte, + row *dbAccountRow) (*dbWatchOnlyAccountRow, error) { + + // The serialized BIP0044 watch-only account raw data format is: + // + // + // + // 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: + // + // + // + // 4 bytes encrypted pubkey len + encrypted pubkey + 4 bytes master key + // fingerprint + 4 bytes next external index + 4 bytes next internal + // index + 4 bytes name len + name + 1 byte addr schema exists + 2 bytes + // addr schema (if exists) + pubLen := uint32(len(encryptedPubKey)) + nameLen := uint32(len(name)) + + addrSchemaExists := addrSchema != nil + var addrSchemaBytes []byte + if addrSchemaExists { + addrSchemaBytes = scopeSchemaToBytes(addrSchema) + } + + bufLen := 21 + pubLen + nameLen + uint32(len(addrSchemaBytes)) + buf := bytes.NewBuffer(make([]byte, 0, bufLen)) + + err := binary.Write(buf, binary.LittleEndian, pubLen) + if err != nil { + return nil, err + } + err = binary.Write(buf, binary.LittleEndian, encryptedPubKey) + if err != nil { + return nil, err + } + + err = binary.Write(buf, binary.LittleEndian, masterKeyFingerprint) + if err != nil { + return nil, err + } + + err = binary.Write(buf, binary.LittleEndian, nextExternalIndex) + if err != nil { + return nil, err + } + err = binary.Write(buf, binary.LittleEndian, nextInternalIndex) + if err != nil { + return nil, err + } + + err = binary.Write(buf, binary.LittleEndian, nameLen) + if err != nil { + return nil, err + } + err = binary.Write(buf, binary.LittleEndian, []byte(name)) + if err != nil { + return nil, err + } + + err = binary.Write(buf, binary.LittleEndian, addrSchemaExists) + if err != nil { + return nil, err + } + if addrSchemaExists { + err = binary.Write(buf, binary.LittleEndian, addrSchemaBytes) + if err != nil { + return nil, err + } + } + + return buf.Bytes(), nil +} + // forEachKeyScope calls the given function for each known manager scope // within the set of scopes known by the root manager. func forEachKeyScope(ns walletdb.ReadBucket, fn func(KeyScope) error) error { @@ -947,6 +1119,8 @@ func fetchAccountInfo(ns walletdb.ReadBucket, scope *KeyScope, switch row.acctType { case accountDefault: return deserializeDefaultAccountRow(accountID, row) + case accountWatchOnly: + return deserializeWatchOnlyAccountRow(accountID, row) } str := fmt.Sprintf("unsupported account type '%d'", row.acctType) @@ -1087,8 +1261,9 @@ func putAccountRow(ns walletdb.ReadWriteBucket, scope *KeyScope, return nil } -// putAccountInfo stores the provided account information to the database. -func putAccountInfo(ns walletdb.ReadWriteBucket, scope *KeyScope, +// putDefaultAccountInfo stores the provided default account information to the +// database. +func putDefaultAccountInfo(ns walletdb.ReadWriteBucket, scope *KeyScope, account uint32, encryptedPubKey, encryptedPrivKey []byte, nextExternalIndex, nextInternalIndex uint32, name string) error { @@ -1103,7 +1278,38 @@ func putAccountInfo(ns walletdb.ReadWriteBucket, scope *KeyScope, acctType: accountDefault, rawData: rawData, } - if err := putAccountRow(ns, scope, account, &acctRow); err != nil { + return putAccountInfo(ns, scope, account, &acctRow, name) +} + +// putWatchOnlyAccountInfo stores the provided watch-only account information to +// the database. +func putWatchOnlyAccountInfo(ns walletdb.ReadWriteBucket, scope *KeyScope, + account uint32, encryptedPubKey []byte, masterKeyFingerprint, + nextExternalIndex, nextInternalIndex uint32, name string, + addrSchema *ScopeAddrSchema) error { + + rawData, err := serializeWatchOnlyAccountRow( + encryptedPubKey, masterKeyFingerprint, nextExternalIndex, + nextInternalIndex, name, addrSchema, + ) + if err != nil { + return err + } + + // TODO(roasbeef): pass scope bucket directly?? + + acctRow := dbAccountRow{ + acctType: accountWatchOnly, + rawData: rawData, + } + return putAccountInfo(ns, scope, account, &acctRow, name) +} + +// putAccountInfo stores the provided account information to the database. +func putAccountInfo(ns walletdb.ReadWriteBucket, scope *KeyScope, + account uint32, acctRow *dbAccountRow, name string) error { + + if err := putAccountRow(ns, scope, account, acctRow); err != nil { return err } @@ -1113,11 +1319,7 @@ func putAccountInfo(ns walletdb.ReadWriteBucket, scope *KeyScope, } // Update account name index. - if err := putAccountNameIndex(ns, scope, account, name); err != nil { - return err - } - - return nil + return putAccountNameIndex(ns, scope, account, name) } // putLastAccount stores the provided metadata - last account - to the @@ -1479,32 +1681,64 @@ func putChainedAddress(ns walletdb.ReadWriteBucket, scope *KeyScope, if err != nil { return err } - arow, err := deserializeDefaultAccountRow(accountID, row) - if err != nil { - return err + + switch row.acctType { + case accountDefault: + arow, err := deserializeDefaultAccountRow(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 = serializeDefaultAccountRow( + arow.pubKeyEncrypted, arow.privKeyEncrypted, + nextExternalIndex, nextInternalIndex, arow.name, + ) + + case accountWatchOnly: + arow, err := deserializeWatchOnlyAccountRow(accountID, row) + if err != nil { + return err + } + + // Increment the appropriate next index depending on whether the + // branch is internal or external. + nextExternalIndex := arow.nextExternalIndex + nextInternalIndex := arow.nextInternalIndex + if branch == InternalBranch { + nextInternalIndex = index + 1 + } else { + nextExternalIndex = index + 1 + } + + // Reserialize the account with the updated index and store it. + row.rawData, err = serializeWatchOnlyAccountRow( + arow.pubKeyEncrypted, arow.masterKeyFingerprint, + nextExternalIndex, nextInternalIndex, arow.name, + arow.addrSchema, + ) + if err != nil { + return err + } } - // 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 = serializeDefaultAccountRow( - arow.pubKeyEncrypted, arow.privKeyEncrypted, nextExternalIndex, - nextInternalIndex, arow.name, - ) err = bucket.Put(accountID, serializeAccountRow(row)) if err != nil { str := fmt.Sprintf("failed to update next index for "+ "address %x, account %d", addressID, account) return managerError(ErrDatabase, str, err) } + return nil } @@ -1741,6 +1975,9 @@ func deletePrivateKeys(ns walletdb.ReadWriteBucket) error { str := "failed to delete account private key" return managerError(ErrDatabase, str, err) } + + // Watch-only accounts don't contain any private keys. + case accountWatchOnly: } return nil diff --git a/waddrmgr/manager.go b/waddrmgr/manager.go index e3aa97f..aa2710e 100644 --- a/waddrmgr/manager.go +++ b/waddrmgr/manager.go @@ -1678,7 +1678,7 @@ func createManagerKeyScope(ns walletdb.ReadWriteBucket, } // Save the information for the default account to the database. - err = putAccountInfo( + err = putDefaultAccountInfo( ns, &scope, DefaultAccountNum, acctPubEnc, acctPrivEnc, 0, 0, defaultAccountName, ) @@ -1686,7 +1686,7 @@ func createManagerKeyScope(ns walletdb.ReadWriteBucket, return err } - return putAccountInfo( + return putDefaultAccountInfo( ns, &scope, ImportedAddrAccount, nil, nil, 0, 0, ImportedAddrAccountName, ) diff --git a/waddrmgr/scoped_manager.go b/waddrmgr/scoped_manager.go index 4ffe192..09dddab 100644 --- a/waddrmgr/scoped_manager.go +++ b/waddrmgr/scoped_manager.go @@ -309,71 +309,95 @@ func (s *ScopedKeyManager) loadAccountInfo(ns walletdb.ReadBucket, return nil, maybeConvertDbError(err) } - // Ensure the account type is a default account. - row, ok := rowInterface.(*dbDefaultAccountRow) - if !ok { - str := fmt.Sprintf("unsupported account type %T", row) - return nil, managerError(ErrDatabase, str, nil) - } + decryptKey := func(cryptoKey EncryptorDecryptor, + encryptedKey []byte) (*hdkeychain.ExtendedKey, error) { - // Use the crypto public key to decrypt the account public extended - // key. - serializedKeyPub, err := s.rootManager.cryptoKeyPub.Decrypt(row.pubKeyEncrypted) - if err != nil { - str := fmt.Sprintf("failed to decrypt public key for account %d", - account) - return nil, managerError(ErrCrypto, str, err) - } - acctKeyPub, err := hdkeychain.NewKeyFromString(string(serializedKeyPub)) - if err != nil { - str := fmt.Sprintf("failed to create extended public key for "+ - "account %d", account) - return nil, managerError(ErrKeyChain, str, err) - } - - // Create the new account info with the known information. The rest of - // the fields are filled out below. - acctInfo := &accountInfo{ - acctName: row.name, - acctKeyEncrypted: row.privKeyEncrypted, - acctKeyPub: acctKeyPub, - nextExternalIndex: row.nextExternalIndex, - nextInternalIndex: row.nextInternalIndex, - } - - 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 - // extended keys. - decrypted, err := s.rootManager.cryptoKeyPriv.Decrypt(acctInfo.acctKeyEncrypted) + serializedKey, err := cryptoKey.Decrypt(encryptedKey) if err != nil { - str := fmt.Sprintf("failed to decrypt private key for "+ + return nil, err + } + return hdkeychain.NewKeyFromString(string(serializedKey)) + } + + // The wallet will only contain private keys for default accounts if the + // wallet's not set up as watch-only and it's been unlocked. + watchOnly := s.rootManager.watchOnly() + hasPrivateKey := !s.rootManager.isLocked() && !watchOnly + + // Create the new account info with the known information. The rest of + // the fields are filled out below. + var acctInfo *accountInfo + switch row := rowInterface.(type) { + case *dbDefaultAccountRow: + acctInfo = &accountInfo{ + acctName: row.name, + acctKeyEncrypted: row.privKeyEncrypted, + nextExternalIndex: row.nextExternalIndex, + nextInternalIndex: row.nextInternalIndex, + } + + // 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) } - acctKeyPriv, err := hdkeychain.NewKeyFromString(string(decrypted)) - if err != nil { - str := fmt.Sprintf("failed to create extended private "+ - "key for account %d", account) - return nil, managerError(ErrKeyChain, str, err) + if hasPrivateKey { + // Use the crypto private key to decrypt the account + // private extended keys. + acctInfo.acctKeyPriv, err = decryptKey( + s.rootManager.cryptoKeyPriv, row.privKeyEncrypted, + ) + if err != nil { + str := fmt.Sprintf("failed to decrypt private "+ + "key for account %d", account) + return nil, managerError(ErrCrypto, str, err) + } } - acctInfo.acctKeyPriv = acctKeyPriv + + case *dbWatchOnlyAccountRow: + acctInfo = &accountInfo{ + acctName: row.name, + nextExternalIndex: row.nextExternalIndex, + nextInternalIndex: row.nextInternalIndex, + } + + // 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) + } + + watchOnly = true + hasPrivateKey = false + + default: + str := fmt.Sprintf("unsupported account type %T", row) + return nil, managerError(ErrDatabase, str, nil) } // Derive and cache the managed address for the last external address. - branch, index := ExternalBranch, row.nextExternalIndex + branch, index := ExternalBranch, acctInfo.nextExternalIndex if index > 0 { index-- } lastExtAddrPath := DerivationPath{ InternalAccount: account, - Account: acctKeyPub.ChildIndex(), + Account: acctInfo.acctKeyPub.ChildIndex(), Branch: branch, Index: index, } - lastExtKey, err := s.deriveKey(acctInfo, branch, index, private) + lastExtKey, err := s.deriveKey(acctInfo, branch, index, hasPrivateKey) if err != nil { return nil, err } @@ -384,17 +408,17 @@ func (s *ScopedKeyManager) loadAccountInfo(ns walletdb.ReadBucket, acctInfo.lastExternalAddr = lastExtAddr // Derive and cache the managed address for the last internal address. - branch, index = InternalBranch, row.nextInternalIndex + branch, index = InternalBranch, acctInfo.nextInternalIndex if index > 0 { index-- } lastIntAddrPath := DerivationPath{ InternalAccount: account, - Account: acctKeyPub.ChildIndex(), + Account: acctInfo.acctKeyPub.ChildIndex(), Branch: branch, Index: index, } - lastIntKey, err := s.deriveKey(acctInfo, branch, index, private) + lastIntKey, err := s.deriveKey(acctInfo, branch, index, hasPrivateKey) if err != nil { return nil, err } @@ -1371,7 +1395,7 @@ func (s *ScopedKeyManager) newAccount(ns walletdb.ReadWriteBucket, // We have the encrypted account extended keys, so save them to the // database - err = putAccountInfo( + err = putDefaultAccountInfo( ns, &s.scope, account, acctPubEnc, acctPrivEnc, 0, 0, name, ) if err != nil { @@ -1435,8 +1459,9 @@ func (s *ScopedKeyManager) newAccountWatchingOnly(ns walletdb.ReadWriteBucket, a // We have the encrypted account extended keys, so save them to the // database - err = putAccountInfo( - ns, &s.scope, account, acctPubEnc, nil, 0, 0, name, + // TODO: set master key fingerprint and addr schema. + err = putWatchOnlyAccountInfo( + ns, &s.scope, account, acctPubEnc, 0, 0, 0, name, nil, ) if err != nil { return err @@ -1478,29 +1503,45 @@ func (s *ScopedKeyManager) RenameAccount(ns walletdb.ReadWriteBucket, return err } - // Ensure the account type is a default account. - row, ok := rowInterface.(*dbDefaultAccountRow) - if !ok { - str := fmt.Sprintf("unsupported account type %T", row) - err = managerError(ErrDatabase, str, nil) - } - // Remove the old name key from the account id index. if err = deleteAccountIDIndex(ns, &s.scope, account); err != nil { return err } - // Remove the old name key from the account name index. - if err = deleteAccountNameIndex(ns, &s.scope, row.name); err != nil { - return err - } - err = putAccountInfo( - ns, &s.scope, account, row.pubKeyEncrypted, - row.privKeyEncrypted, row.nextExternalIndex, - row.nextInternalIndex, name, - ) - if err != nil { - return err + switch row := rowInterface.(type) { + case *dbDefaultAccountRow: + // Remove the old name key from the account name index. + if err = deleteAccountNameIndex(ns, &s.scope, row.name); err != nil { + return err + } + + err = putDefaultAccountInfo( + ns, &s.scope, account, row.pubKeyEncrypted, + row.privKeyEncrypted, row.nextExternalIndex, + row.nextInternalIndex, name, + ) + if err != nil { + return err + } + + case *dbWatchOnlyAccountRow: + // Remove the old name key from the account name index. + if err = deleteAccountNameIndex(ns, &s.scope, row.name); err != nil { + return err + } + + err = putWatchOnlyAccountInfo( + ns, &s.scope, account, row.pubKeyEncrypted, + row.masterKeyFingerprint, row.nextExternalIndex, + row.nextInternalIndex, name, row.addrSchema, + ) + if err != nil { + return err + } + + default: + str := fmt.Sprintf("unsupported account type %T", row) + return managerError(ErrDatabase, str, nil) } // Update in-memory account info with new name if cached and the db From 89e1671f0cb8b65491c462565763157bd0fbfe17 Mon Sep 17 00:00:00 2001 From: Wilmer Paulino Date: Mon, 15 Feb 2021 14:38:23 -0800 Subject: [PATCH 09/16] waddrmgr: extend watch-only account init with new parameters The master fingerprint corresponds to the fingerprint of the root master public key (otherwise known as m/). This is required by some hardware wallets for proper identification and signing. The address schema is an optional field that allows an account to override its corresponding address schema with a custom one. --- waddrmgr/manager_test.go | 11 +++++-- waddrmgr/scoped_manager.go | 66 +++++++++++++++++++++++++++++--------- 2 files changed, 59 insertions(+), 18 deletions(-) diff --git a/waddrmgr/manager_test.go b/waddrmgr/manager_test.go index dd2139f..d574302 100644 --- a/waddrmgr/manager_test.go +++ b/waddrmgr/manager_test.go @@ -1947,7 +1947,8 @@ func testManagerCase(t *testing.T, caseName string, err = walletdb.Update(db, func(tx walletdb.ReadWriteTx) error { ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) _, err = scopedMgr.NewAccountWatchingOnly( - ns, defaultAccountName, acctKeyPub) + ns, defaultAccountName, acctKeyPub, 0, nil, + ) return err }) if err != nil { @@ -2648,7 +2649,9 @@ func TestNewRawAccountWatchingOnly(t *testing.T) { const accountNum = 1000 err = walletdb.Update(db, func(tx walletdb.ReadWriteTx) error { ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) - return scopedMgr.NewRawAccountWatchingOnly(ns, accountNum, accountKey) + return scopedMgr.NewRawAccountWatchingOnly( + ns, accountNum, accountKey, 0, nil, + ) }) if err != nil { t.Fatalf("unable to create new account: %v", err) @@ -2713,7 +2716,9 @@ func TestNewRawAccountHybrid(t *testing.T) { const accountNum = 1000 err = walletdb.Update(db, func(tx walletdb.ReadWriteTx) error { ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) - return scopedMgr.NewRawAccountWatchingOnly(ns, accountNum, acctKeyPub) + return scopedMgr.NewRawAccountWatchingOnly( + ns, accountNum, acctKeyPub, 0, nil, + ) }) if err != nil { t.Fatalf("unable to create new account: %v", err) diff --git a/waddrmgr/scoped_manager.go b/waddrmgr/scoped_manager.go index 09dddab..298d175 100644 --- a/waddrmgr/scoped_manager.go +++ b/waddrmgr/scoped_manager.go @@ -1271,14 +1271,24 @@ func (s *ScopedKeyManager) NewRawAccount(ns walletdb.ReadWriteBucket, number uin return s.newAccount(ns, number, name) } -// NewRawAccountWatchingOnly creates a new watching only account for -// the scoped manager. This method differs from the -// NewAccountWatchingOnly method in that this method takes the account -// number *directly*, rather than taking a string name for the -// account, then mapping that to the next highest account number. +// 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) error { + pubKey *hdkeychain.ExtendedKey, masterKeyFingerprint uint32, + addrSchema *ScopeAddrSchema) error { + s.mtx.Lock() defer s.mtx.Unlock() @@ -1286,7 +1296,9 @@ func (s *ScopedKeyManager) NewRawAccountWatchingOnly( // 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) + return s.newAccountWatchingOnly( + ns, number, name, pubKey, masterKeyFingerprint, addrSchema, + ) } // NewAccount creates and returns a new account stored in the manager based on @@ -1407,8 +1419,19 @@ func (s *ScopedKeyManager) newAccount(ns walletdb.ReadWriteBucket, } // NewAccountWatchingOnly is similar to NewAccount, but for watch-only wallets. -func (s *ScopedKeyManager) NewAccountWatchingOnly(ns walletdb.ReadWriteBucket, name string, - pubKey *hdkeychain.ExtendedKey) (uint32, error) { +// +// 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() @@ -1423,7 +1446,10 @@ func (s *ScopedKeyManager) NewAccountWatchingOnly(ns walletdb.ReadWriteBucket, n // With the name validated, we'll create a new account for the new // contiguous account. - if err := s.newAccountWatchingOnly(ns, account, name, pubKey); err != nil { + err = s.newAccountWatchingOnly( + ns, account, name, pubKey, masterKeyFingerprint, addrSchema, + ) + if err != nil { return 0, err } @@ -1432,9 +1458,19 @@ func (s *ScopedKeyManager) NewAccountWatchingOnly(ns walletdb.ReadWriteBucket, n // 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) error { +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 { @@ -1453,15 +1489,15 @@ func (s *ScopedKeyManager) newAccountWatchingOnly(ns walletdb.ReadWriteBucket, a []byte(pubKey.String()), ) if err != nil { - str := "failed to encrypt public key for account" + 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 - // TODO: set master key fingerprint and addr schema. err = putWatchOnlyAccountInfo( - ns, &s.scope, account, acctPubEnc, 0, 0, 0, name, nil, + ns, &s.scope, account, acctPubEnc, masterKeyFingerprint, 0, 0, + name, addrSchema, ) if err != nil { return err From e2d54f001b22224c448a93987bb83a236c098436 Mon Sep 17 00:00:00 2001 From: Wilmer Paulino Date: Wed, 10 Mar 2021 17:03:45 -0800 Subject: [PATCH 10/16] waddrmgr: derive account addresses with schema override This change was motivated by the need to support importing BIP-0049 keys that use the standard address derivation scheme, where nested witness pubkeys are used for both the external and internal branches. Our BIP-0049 key scope is slightly different, in that addresses derived from the internal branch use the witness pubkey address type. By having the option of overriding the address schema for a particular account, we can support importing standard BIP-0049 keys. --- waddrmgr/manager.go | 6 ++++ waddrmgr/scoped_manager.go | 65 +++++++++++++++++++++++++++----------- 2 files changed, 52 insertions(+), 19 deletions(-) diff --git a/waddrmgr/manager.go b/waddrmgr/manager.go index aa2710e..bf1ca6f 100644 --- a/waddrmgr/manager.go +++ b/waddrmgr/manager.go @@ -160,6 +160,12 @@ type accountInfo struct { // intended for internal wallet use such as change addresses. nextInternalIndex uint32 lastInternalAddr ManagedAddress + + // addrSchema serves as a way for an account to override its + // corresponding address schema with a custom one. For example, this + // could be used to import accounts that use the traditional BIP-0049 + // derivation scheme into our KeyScopeBIP-0049Plus manager. + addrSchema *ScopeAddrSchema } // AccountProperties contains properties associated with each account, such as diff --git a/waddrmgr/scoped_manager.go b/waddrmgr/scoped_manager.go index 298d175..239cda6 100644 --- a/waddrmgr/scoped_manager.go +++ b/waddrmgr/scoped_manager.go @@ -217,14 +217,14 @@ func (s *ScopedKeyManager) Close() { // // This function MUST be called with the manager lock held for writes. func (s *ScopedKeyManager) keyToManaged(derivedKey *hdkeychain.ExtendedKey, - derivationPath DerivationPath) (ManagedAddress, error) { + derivationPath DerivationPath, acctInfo *accountInfo) ( + ManagedAddress, error) { - var addrType AddressType - if derivationPath.Branch == InternalBranch { - addrType = s.addrSchema.InternalAddrType - } else { - addrType = s.addrSchema.ExternalAddrType - } + // Choose the appropriate type of address to derive since it's possible + // for a watch-only account to have a different schema from the + // manager's. + internal := derivationPath.Branch == InternalBranch + addrType := s.accountAddrType(acctInfo, internal) // Create a new managed address based on the public or private key // depending on whether the passed key is private. Also, zero the key @@ -365,6 +365,7 @@ func (s *ScopedKeyManager) loadAccountInfo(ns walletdb.ReadBucket, acctName: row.name, nextExternalIndex: row.nextExternalIndex, nextInternalIndex: row.nextInternalIndex, + addrSchema: row.addrSchema, } // Use the crypto public key to decrypt the account public @@ -401,7 +402,7 @@ func (s *ScopedKeyManager) loadAccountInfo(ns walletdb.ReadBucket, if err != nil { return nil, err } - lastExtAddr, err := s.keyToManaged(lastExtKey, lastExtAddrPath) + lastExtAddr, err := s.keyToManaged(lastExtKey, lastExtAddrPath, acctInfo) if err != nil { return nil, err } @@ -422,7 +423,7 @@ func (s *ScopedKeyManager) loadAccountInfo(ns walletdb.ReadBucket, if err != nil { return nil, err } - lastIntAddr, err := s.keyToManaged(lastIntKey, lastIntAddrPath) + lastIntAddr, err := s.keyToManaged(lastIntKey, lastIntAddrPath, acctInfo) if err != nil { return nil, err } @@ -503,7 +504,11 @@ func (s *ScopedKeyManager) DeriveFromKeyPath(ns walletdb.ReadBucket, return nil, err } - return s.keyToManaged(addrKey, kp) + acctInfo, err := s.loadAccountInfo(ns, kp.InternalAccount) + if err != nil { + return nil, err + } + return s.keyToManaged(addrKey, kp, acctInfo) } // deriveKeyFromPath returns either a public or private derived extended key @@ -552,13 +557,18 @@ func (s *ScopedKeyManager) chainAddressRowToManaged(ns walletdb.ReadBucket, if err != nil { return nil, err } + + acctInfo, err := s.loadAccountInfo(ns, row.account) + if err != nil { + return nil, err + } return s.keyToManaged( addressKey, DerivationPath{ InternalAccount: row.account, Account: acctKey.ChildIndex(), Branch: row.branch, Index: row.index, - }, + }, acctInfo, ) } @@ -728,6 +738,23 @@ func (s *ScopedKeyManager) AddrAccount(ns walletdb.ReadBucket, return account, nil } +// accountAddrType determines the type of address that should be generated for +// an account based on whether it's an internal address or not. +func (s *ScopedKeyManager) accountAddrType(acctInfo *accountInfo, + internal bool) AddressType { + + // If the account has a custom address schema, use it. + addrSchema := s.addrSchema + if acctInfo.addrSchema != nil { + addrSchema = *acctInfo.addrSchema + } + + if internal { + return addrSchema.InternalAddrType + } + return addrSchema.ExternalAddrType +} + // nextAddresses returns the specified number of next chained address from the // branch indicated by the internal flag. // @@ -758,10 +785,10 @@ func (s *ScopedKeyManager) nextAddresses(ns walletdb.ReadWriteBucket, nextIndex = acctInfo.nextInternalIndex } - addrType := s.addrSchema.ExternalAddrType - if internal { - addrType = s.addrSchema.InternalAddrType - } + // Choose the appropriate type of address to derive since it's possible + // for a watch-only account to have a different schema from the + // manager's. + addrType := s.accountAddrType(acctInfo, internal) // Ensure the requested number of addresses doesn't exceed the maximum // allowed for this account. @@ -955,10 +982,10 @@ func (s *ScopedKeyManager) extendAddresses(ns walletdb.ReadWriteBucket, nextIndex = acctInfo.nextInternalIndex } - addrType := s.addrSchema.ExternalAddrType - if internal { - addrType = s.addrSchema.InternalAddrType - } + // Choose the appropriate type of address to derive since it's possible + // for a watch-only account to have a different schema from the + // manager's. + addrType := s.accountAddrType(acctInfo, internal) // If the last index requested is already lower than the next index, we // can return early. From 0b5eca1835bb650251e8d33d5b83d00aee63a629 Mon Sep 17 00:00:00 2001 From: Wilmer Paulino Date: Tue, 16 Feb 2021 17:03:35 -0800 Subject: [PATCH 11/16] wallet: move ImportPrivateKey to import.go --- wallet/import.go | 113 +++++++++++++++++++++++++++++++++++++++++++++++ wallet/wallet.go | 104 ------------------------------------------- 2 files changed, 113 insertions(+), 104 deletions(-) create mode 100644 wallet/import.go diff --git a/wallet/import.go b/wallet/import.go new file mode 100644 index 0000000..49442fd --- /dev/null +++ b/wallet/import.go @@ -0,0 +1,113 @@ +package wallet + +import ( + "fmt" + + "github.com/btcsuite/btcutil" + "github.com/btcsuite/btcwallet/waddrmgr" + "github.com/btcsuite/btcwallet/walletdb" +) + +// ImportPrivateKey imports a private key to the wallet and writes the new +// wallet to disk. +// +// NOTE: If a block stamp is not provided, then the wallet's birthday will be +// set to the genesis block of the corresponding chain. +func (w *Wallet) ImportPrivateKey(scope waddrmgr.KeyScope, wif *btcutil.WIF, + bs *waddrmgr.BlockStamp, rescan bool) (string, error) { + + manager, err := w.Manager.FetchScopedKeyManager(scope) + if err != nil { + return "", err + } + + // The starting block for the key is the genesis block unless otherwise + // specified. + if bs == nil { + bs = &waddrmgr.BlockStamp{ + Hash: *w.chainParams.GenesisHash, + Height: 0, + Timestamp: w.chainParams.GenesisBlock.Header.Timestamp, + } + } else if bs.Timestamp.IsZero() { + // Only update the new birthday time from default value if we + // actually have timestamp info in the header. + header, err := w.chainClient.GetBlockHeader(&bs.Hash) + if err == nil { + bs.Timestamp = header.Timestamp + } + } + + // Attempt to import private key into wallet. + var addr btcutil.Address + var props *waddrmgr.AccountProperties + err = walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error { + addrmgrNs := tx.ReadWriteBucket(waddrmgrNamespaceKey) + maddr, err := manager.ImportPrivateKey(addrmgrNs, wif, bs) + if err != nil { + return err + } + addr = maddr.Address() + props, err = manager.AccountProperties( + addrmgrNs, waddrmgr.ImportedAddrAccount, + ) + if err != nil { + return err + } + + // We'll only update our birthday with the new one if it is + // before our current one. Otherwise, if we do, we can + // potentially miss detecting relevant chain events that + // occurred between them while rescanning. + birthdayBlock, _, err := w.Manager.BirthdayBlock(addrmgrNs) + if err != nil { + return err + } + if bs.Height >= birthdayBlock.Height { + return nil + } + + err = w.Manager.SetBirthday(addrmgrNs, bs.Timestamp) + if err != nil { + return err + } + + // To ensure this birthday block is correct, we'll mark it as + // unverified to prompt a sanity check at the next restart to + // ensure it is correct as it was provided by the caller. + return w.Manager.SetBirthdayBlock(addrmgrNs, *bs, false) + }) + if err != nil { + return "", err + } + + // Rescan blockchain for transactions with txout scripts paying to the + // imported address. + if rescan { + job := &RescanJob{ + Addrs: []btcutil.Address{addr}, + OutPoints: nil, + BlockStamp: *bs, + } + + // Submit rescan job and log when the import has completed. + // Do not block on finishing the rescan. The rescan success + // or failure is logged elsewhere, and the channel is not + // required to be read, so discard the return value. + _ = w.SubmitRescan(job) + } else { + err := w.chainClient.NotifyReceived([]btcutil.Address{addr}) + if err != nil { + return "", fmt.Errorf("Failed to subscribe for address ntfns for "+ + "address %s: %s", addr.EncodeAddress(), err) + } + } + + addrStr := addr.EncodeAddress() + log.Infof("Imported payment address %s", addrStr) + + w.NtfnServer.notifyAccountProperties(props) + + // Return the payment address string of the imported private key. + return addrStr, nil +} diff --git a/wallet/wallet.go b/wallet/wallet.go index 7291de5..3f6ed5d 100644 --- a/wallet/wallet.go +++ b/wallet/wallet.go @@ -2722,110 +2722,6 @@ func (w *Wallet) DumpWIFPrivateKey(addr btcutil.Address) (string, error) { return wif.String(), nil } -// ImportPrivateKey imports a private key to the wallet and writes the new -// wallet to disk. -// -// NOTE: If a block stamp is not provided, then the wallet's birthday will be -// set to the genesis block of the corresponding chain. -func (w *Wallet) ImportPrivateKey(scope waddrmgr.KeyScope, wif *btcutil.WIF, - bs *waddrmgr.BlockStamp, rescan bool) (string, error) { - - manager, err := w.Manager.FetchScopedKeyManager(scope) - if err != nil { - return "", err - } - - // The starting block for the key is the genesis block unless otherwise - // specified. - if bs == nil { - bs = &waddrmgr.BlockStamp{ - Hash: *w.chainParams.GenesisHash, - Height: 0, - Timestamp: w.chainParams.GenesisBlock.Header.Timestamp, - } - } else if bs.Timestamp.IsZero() { - // Only update the new birthday time from default value if we - // actually have timestamp info in the header. - header, err := w.chainClient.GetBlockHeader(&bs.Hash) - if err == nil { - bs.Timestamp = header.Timestamp - } - } - - // Attempt to import private key into wallet. - var addr btcutil.Address - var props *waddrmgr.AccountProperties - err = walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error { - addrmgrNs := tx.ReadWriteBucket(waddrmgrNamespaceKey) - maddr, err := manager.ImportPrivateKey(addrmgrNs, wif, bs) - if err != nil { - return err - } - addr = maddr.Address() - props, err = manager.AccountProperties( - addrmgrNs, waddrmgr.ImportedAddrAccount, - ) - if err != nil { - return err - } - - // We'll only update our birthday with the new one if it is - // before our current one. Otherwise, if we do, we can - // potentially miss detecting relevant chain events that - // occurred between them while rescanning. - birthdayBlock, _, err := w.Manager.BirthdayBlock(addrmgrNs) - if err != nil { - return err - } - if bs.Height >= birthdayBlock.Height { - return nil - } - - err = w.Manager.SetBirthday(addrmgrNs, bs.Timestamp) - if err != nil { - return err - } - - // To ensure this birthday block is correct, we'll mark it as - // unverified to prompt a sanity check at the next restart to - // ensure it is correct as it was provided by the caller. - return w.Manager.SetBirthdayBlock(addrmgrNs, *bs, false) - }) - if err != nil { - return "", err - } - - // Rescan blockchain for transactions with txout scripts paying to the - // imported address. - if rescan { - job := &RescanJob{ - Addrs: []btcutil.Address{addr}, - OutPoints: nil, - BlockStamp: *bs, - } - - // Submit rescan job and log when the import has completed. - // Do not block on finishing the rescan. The rescan success - // or failure is logged elsewhere, and the channel is not - // required to be read, so discard the return value. - _ = w.SubmitRescan(job) - } else { - err := w.chainClient.NotifyReceived([]btcutil.Address{addr}) - if err != nil { - return "", fmt.Errorf("Failed to subscribe for address ntfns for "+ - "address %s: %s", addr.EncodeAddress(), err) - } - } - - addrStr := addr.EncodeAddress() - log.Infof("Imported payment address %s", addrStr) - - w.NtfnServer.notifyAccountProperties(props) - - // Return the payment address string of the imported private key. - return addrStr, nil -} - // LockedOutpoint returns whether an outpoint has been marked as locked and // should not be used as an input for created transactions. func (w *Wallet) LockedOutpoint(op wire.OutPoint) bool { From 283f914b9569463fff7ac970ff4294f93021112b Mon Sep 17 00:00:00 2001 From: Wilmer Paulino Date: Mon, 22 Feb 2021 16:16:41 -0800 Subject: [PATCH 12/16] waddrmgr: return err when priv key is requested for watch-only addr --- waddrmgr/address.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/waddrmgr/address.go b/waddrmgr/address.go index 49c6a26..3763a1c 100644 --- a/waddrmgr/address.go +++ b/waddrmgr/address.go @@ -151,6 +151,12 @@ func (a *managedAddress) unlock(key EncryptorDecryptor) ([]byte, error) { a.privKeyMutex.Lock() defer a.privKeyMutex.Unlock() + // If the address belongs to a watch-only account, the encrypted private + // key won't be present, so we'll return an error. + if len(a.privKeyEncrypted) == 0 { + return nil, managerError(ErrWatchingOnly, errWatchingOnly, nil) + } + if len(a.privKeyCT) == 0 { privKey, err := key.Decrypt(a.privKeyEncrypted) if err != nil { From adb3d77c86c4cc2ea88550e3e5a5d3bfc9f4d04f Mon Sep 17 00:00:00 2001 From: Wilmer Paulino Date: Wed, 17 Feb 2021 16:24:28 -0800 Subject: [PATCH 13/16] waddrmgr: extend AccountProperties with watch-only account properties --- waddrmgr/manager.go | 45 ++++++++++++++++++++++++++++++++++++-- waddrmgr/scoped_manager.go | 22 ++++++++++++++----- 2 files changed, 59 insertions(+), 8 deletions(-) diff --git a/waddrmgr/manager.go b/waddrmgr/manager.go index bf1ca6f..5de694f 100644 --- a/waddrmgr/manager.go +++ b/waddrmgr/manager.go @@ -166,16 +166,57 @@ type accountInfo struct { // could be used to import accounts that use the traditional BIP-0049 // derivation scheme into our KeyScopeBIP-0049Plus manager. addrSchema *ScopeAddrSchema + + // masterKeyFingerprint represents the fingerprint of the root key + // corresponding to the master public key (also known as the key with + // derivation path m/). This may be required by some hardware wallets + // for proper identification and signing. + masterKeyFingerprint uint32 } // AccountProperties contains properties associated with each account, such as // the account name, number, and the nubmer of derived and imported keys. type AccountProperties struct { - AccountNumber uint32 - AccountName string + // AccountNumber is the internal number used to reference the account. + AccountNumber uint32 + + // AccountName is the user-identifying name of the account. + AccountName string + + // ExternalKeyCount is the number of internal keys that have been + // derived for the account. ExternalKeyCount uint32 + + // InternalKeyCount is the number of internal keys that have been + // derived for the account. InternalKeyCount uint32 + + // ImportedKeyCount is the number of imported keys found within the + // account. ImportedKeyCount uint32 + + // AccountPubKey is the account's public key that can be used to + // derive any address relevant to said account. + // + // NOTE: This may be nil for imported accounts. + AccountPubKey *hdkeychain.ExtendedKey + + // MasterKeyFingerprint represents the fingerprint of the root key + // corresponding to the master public key (also known as the key with + // derivation path m/). This may be required by some hardware wallets + // for proper identification and signing. + MasterKeyFingerprint uint32 + + // KeyScope is the key scope the account belongs to. + KeyScope KeyScope + + // IsWatchOnly indicates whether the is set up as watch-only, i.e., it + // doesn't contain any private key information. + IsWatchOnly bool + + // AddrSchema, if non-nil, specifies an address schema override for + // address generation only applicable to the account. + AddrSchema *ScopeAddrSchema } // unlockDeriveInfo houses the information needed to derive a private key for a diff --git a/waddrmgr/scoped_manager.go b/waddrmgr/scoped_manager.go index 239cda6..41b1fba 100644 --- a/waddrmgr/scoped_manager.go +++ b/waddrmgr/scoped_manager.go @@ -71,7 +71,7 @@ type ScopedIndex struct { // String returns a human readable version describing the keypath encapsulated // by the target key scope. -func (k *KeyScope) String() string { +func (k KeyScope) String() string { return fmt.Sprintf("m/%v'/%v'", k.Purpose, k.Coin) } @@ -362,10 +362,11 @@ func (s *ScopedKeyManager) loadAccountInfo(ns walletdb.ReadBucket, case *dbWatchOnlyAccountRow: acctInfo = &accountInfo{ - acctName: row.name, - nextExternalIndex: row.nextExternalIndex, - nextInternalIndex: row.nextInternalIndex, - addrSchema: row.addrSchema, + acctName: row.name, + nextExternalIndex: row.nextExternalIndex, + nextInternalIndex: row.nextInternalIndex, + addrSchema: row.addrSchema, + masterKeyFingerprint: row.masterKeyFingerprint, } // Use the crypto public key to decrypt the account public @@ -442,7 +443,10 @@ func (s *ScopedKeyManager) AccountProperties(ns walletdb.ReadBucket, defer s.mtx.RUnlock() s.mtx.RLock() - props := &AccountProperties{AccountNumber: account} + props := &AccountProperties{ + AccountNumber: account, + KeyScope: s.scope, + } // Until keys can be imported into any account, special handling is // required for the imported account. @@ -463,8 +467,14 @@ func (s *ScopedKeyManager) AccountProperties(ns walletdb.ReadBucket, props.AccountName = acctInfo.acctName props.ExternalKeyCount = acctInfo.nextExternalIndex props.InternalKeyCount = acctInfo.nextInternalIndex + props.AccountPubKey = acctInfo.acctKeyPub + props.MasterKeyFingerprint = acctInfo.masterKeyFingerprint + props.IsWatchOnly = s.rootManager.WatchOnly() || + acctInfo.acctKeyPriv == nil + props.AddrSchema = acctInfo.addrSchema } else { props.AccountName = ImportedAddrAccountName // reserved, nonchangable + props.IsWatchOnly = s.rootManager.WatchOnly() // Could be more efficient if this was tracked by the db. var importedKeyCount uint32 From 9d909110f9cdbfa832af373170774a816acf6dd6 Mon Sep 17 00:00:00 2001 From: Wilmer Paulino Date: Thu, 11 Mar 2021 13:50:09 -0800 Subject: [PATCH 14/16] waddrmgr: use proper version for account key of non-watch-only accounts --- waddrmgr/manager.go | 2 + waddrmgr/scoped_manager.go | 103 +++++++++++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+) diff --git a/waddrmgr/manager.go b/waddrmgr/manager.go index 5de694f..72a7984 100644 --- a/waddrmgr/manager.go +++ b/waddrmgr/manager.go @@ -144,6 +144,8 @@ type addrKey string type accountInfo struct { acctName string + acctType accountType + // The account key is used to derive the branches which in turn derive // the internal and external addresses. The accountKeyPriv will be nil // when the address manager is locked. diff --git a/waddrmgr/scoped_manager.go b/waddrmgr/scoped_manager.go index 41b1fba..d9b40f5 100644 --- a/waddrmgr/scoped_manager.go +++ b/waddrmgr/scoped_manager.go @@ -1,18 +1,50 @@ package waddrmgr import ( + "encoding/binary" "fmt" "sync" "github.com/btcsuite/btcd/btcec" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" "github.com/btcsuite/btcutil/hdkeychain" "github.com/btcsuite/btcwallet/internal/zero" "github.com/btcsuite/btcwallet/walletdb" ) +// HDVersion represents the different supported schemes of hierarchical +// derivation. +type HDVersion uint32 + +const ( + // HDVersionMainNetBIP0044 is the HDVersion for BIP-0044 on the main + // network. + HDVersionMainNetBIP0044 HDVersion = 0x0488b21e // xpub + + // HDVersionMainNetBIP0049 is the HDVersion for BIP-0049 on the main + // network. + HDVersionMainNetBIP0049 HDVersion = 0x049d7cb2 // ypub + + // HDVersionMainNetBIP0084 is the HDVersion for BIP-0084 on the main + // network. + HDVersionMainNetBIP0084 HDVersion = 0x04b24746 // zpub + + // HDVersionTestNetBIP0044 is the HDVersion for BIP-0044 on the test + // network. + HDVersionTestNetBIP0044 HDVersion = 0x043587cf // tpub + + // HDVersionTestNetBIP0049 is the HDVersion for BIP-0049 on the test + // network. + HDVersionTestNetBIP0049 HDVersion = 0x044a5262 // upub + + // HDVersionTestNetBIP0084 is the HDVersion for BIP-0084 on the test + // network. + HDVersionTestNetBIP0084 HDVersion = 0x045f1cf6 // vpub +) + // DerivationPath represents a derivation path from a particular key manager's // scope. Each ScopedKeyManager starts key derivation from the end of their // cointype hardened key: m/purpose'/cointype'. The fields in this struct allow @@ -331,6 +363,7 @@ func (s *ScopedKeyManager) loadAccountInfo(ns walletdb.ReadBucket, case *dbDefaultAccountRow: acctInfo = &accountInfo{ acctName: row.name, + acctType: row.acctType, acctKeyEncrypted: row.privKeyEncrypted, nextExternalIndex: row.nextExternalIndex, nextInternalIndex: row.nextInternalIndex, @@ -363,6 +396,7 @@ 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, @@ -472,6 +506,29 @@ func (s *ScopedKeyManager) AccountProperties(ns walletdb.ReadBucket, props.IsWatchOnly = s.rootManager.WatchOnly() || acctInfo.acctKeyPriv == nil props.AddrSchema = acctInfo.addrSchema + + // Export the account public key with the correct version + // corresponding to the manager's key scope for non-watch-only + // accounts. This isn't done for watch-only accounts to maintain + // the account public key consistent with what the caller + // provided. Note that his is only done for the default key + // scopes, as we only know the HD versions for those. + isDefaultKeyScope := false + for _, scope := range DefaultKeyScopes { + if s.scope == scope { + isDefaultKeyScope = true + break + } + } + if acctInfo.acctType == accountDefault && isDefaultKeyScope { + props.AccountPubKey, err = s.cloneKeyWithVersion( + acctInfo.acctKeyPub, + ) + if err != nil { + return nil, fmt.Errorf("failed to retrieve "+ + "account public key: %v", err) + } + } } else { props.AccountName = ImportedAddrAccountName // reserved, nonchangable props.IsWatchOnly = s.rootManager.WatchOnly() @@ -2118,3 +2175,49 @@ func (s *ScopedKeyManager) ForEachInternalActiveAddress(ns walletdb.ReadBucket, return nil } + +// cloneKeyWithVersion clones an extended key to use the version corresponding +// to the manager's key scope. This should only be used for non-watch-only +// accounts as they are stored within the database using the legacy BIP-0044 +// version by default. +func (s *ScopedKeyManager) cloneKeyWithVersion(key *hdkeychain.ExtendedKey) ( + *hdkeychain.ExtendedKey, error) { + + // Determine the appropriate version based on the current network and + // key scope. + var version HDVersion + net := s.rootManager.ChainParams().Net + switch net { + case wire.MainNet: + switch s.scope { + case KeyScopeBIP0044: + version = HDVersionMainNetBIP0044 + case KeyScopeBIP0049Plus: + version = HDVersionMainNetBIP0049 + case KeyScopeBIP0084: + version = HDVersionMainNetBIP0084 + default: + return nil, fmt.Errorf("unsupported scope %v", s.scope) + } + + case wire.TestNet, wire.TestNet3: + switch s.scope { + case KeyScopeBIP0044: + version = HDVersionTestNetBIP0044 + case KeyScopeBIP0049Plus: + version = HDVersionTestNetBIP0049 + case KeyScopeBIP0084: + version = HDVersionTestNetBIP0084 + default: + return nil, fmt.Errorf("unsupported scope %v", s.scope) + } + + default: + return nil, fmt.Errorf("unsupported net %v", net) + } + + var versionBytes [4]byte + binary.BigEndian.PutUint32(versionBytes[:], uint32(version)) + + return key.CloneWithVersion(versionBytes[:]) +} From b0a4956231b9c34fa0d50de6a861d3fbf26045c0 Mon Sep 17 00:00:00 2001 From: Wilmer Paulino Date: Tue, 16 Feb 2021 17:03:37 -0800 Subject: [PATCH 15/16] wallet: add derived public key import Co-authored-by: Oliver Gugger --- go.mod | 1 + waddrmgr/scoped_manager.go | 12 +- wallet/example_test.go | 47 +++++++ wallet/import.go | 252 +++++++++++++++++++++++++++++++++ wallet/import_test.go | 282 +++++++++++++++++++++++++++++++++++++ 5 files changed, 593 insertions(+), 1 deletion(-) create mode 100644 wallet/import_test.go diff --git a/go.mod b/go.mod index dae8619..82182d9 100644 --- a/go.mod +++ b/go.mod @@ -17,6 +17,7 @@ require ( github.com/kkdai/bstream v0.0.0-20181106074824-b3251f7901ec // indirect github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf github.com/lightninglabs/neutrino v0.11.0 + github.com/stretchr/testify v1.5.1 golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 google.golang.org/genproto v0.0.0-20190201180003-4b09977fb922 // indirect diff --git a/waddrmgr/scoped_manager.go b/waddrmgr/scoped_manager.go index d9b40f5..0a6c6cb 100644 --- a/waddrmgr/scoped_manager.go +++ b/waddrmgr/scoped_manager.go @@ -168,6 +168,15 @@ var ( ExternalAddrType: PubKeyHash, }, } + + // KeyScopeBIP0049AddrSchema is the address schema for the traditional + // BIP-0049 derivation scheme. This exists in order to support importing + // accounts from other wallets that don't use our modified BIP-0049 + // derivation scheme (internal addresses are P2WKH instead of NP2WKH). + KeyScopeBIP0049AddrSchema = ScopeAddrSchema{ + ExternalAddrType: NestedWitnessPubKey, + InternalAddrType: NestedWitnessPubKey, + } ) // ScopedKeyManager is a sub key manager under the main root key manager. The @@ -1842,7 +1851,8 @@ func (s *ScopedKeyManager) importPublicKey(ns walletdb.ReadWriteBucket, // The start block needs to be updated when the newly imported address // is before the current one. s.rootManager.mtx.Lock() - updateStartBlock := bs.Height < s.rootManager.syncState.startBlock.Height + updateStartBlock := bs != nil && + bs.Height < s.rootManager.syncState.startBlock.Height s.rootManager.mtx.Unlock() // Save the new imported address to the db and update start block (if diff --git a/wallet/example_test.go b/wallet/example_test.go index 5b29d2e..2a9f30c 100644 --- a/wallet/example_test.go +++ b/wallet/example_test.go @@ -8,6 +8,8 @@ import ( "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcutil/hdkeychain" + "github.com/btcsuite/btcwallet/waddrmgr" + "github.com/btcsuite/btcwallet/walletdb" ) // defaultDBTimeout specifies the timeout value when opening the wallet @@ -51,3 +53,48 @@ func testWallet(t *testing.T) (*Wallet, func()) { return w, cleanup } + +// testWalletWatchingOnly creates a test watch only wallet and unlocks it. +func testWalletWatchingOnly(t *testing.T) (*Wallet, func()) { + // Set up a wallet. + dir, err := ioutil.TempDir("", "test_wallet_watch_only") + if err != nil { + t.Fatalf("Failed to create db dir: %v", err) + } + + cleanup := func() { + if err := os.RemoveAll(dir); err != nil { + t.Fatalf("could not cleanup test: %v", err) + } + } + + pubPass := []byte("hello") + loader := NewLoader( + &chaincfg.TestNet3Params, dir, true, defaultDBTimeout, 250, + ) + w, err := loader.CreateNewWatchingOnlyWallet(pubPass, time.Now()) + if err != nil { + t.Fatalf("unable to create wallet: %v", err) + } + chainClient := &mockChainClient{} + w.chainClient = chainClient + + err = walletdb.Update(w.Database(), func(tx walletdb.ReadWriteTx) error { + ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) + for scope, schema := range waddrmgr.ScopeAddrMap { + _, err := w.Manager.NewScopedKeyManager( + ns, scope, schema, + ) + if err != nil { + return err + } + } + + return nil + }) + if err != nil { + t.Fatalf("unable to create default scopes: %v", err) + } + + return w, cleanup +} diff --git a/wallet/import.go b/wallet/import.go index 49442fd..ce867ed 100644 --- a/wallet/import.go +++ b/wallet/import.go @@ -1,13 +1,265 @@ package wallet import ( + "encoding/binary" + "errors" "fmt" + "github.com/btcsuite/btcd/btcec" + "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" + "github.com/btcsuite/btcutil/hdkeychain" "github.com/btcsuite/btcwallet/waddrmgr" "github.com/btcsuite/btcwallet/walletdb" ) +const ( + // accountPubKeyDepth is the maximum depth of an extended key for an + // account public key. + accountPubKeyDepth = 3 + + // pubKeyDepth is the depth of an extended key for a derived public key. + pubKeyDepth = 5 +) + +// keyScopeFromPubKey returns the corresponding wallet key scope for the given +// extended public key. The address type can usually be inferred from the key's +// version, but may be required for certain keys to map them into the proper +// scope. +func keyScopeFromPubKey(pubKey *hdkeychain.ExtendedKey, + addrType *waddrmgr.AddressType) (waddrmgr.KeyScope, + *waddrmgr.ScopeAddrSchema, error) { + + switch waddrmgr.HDVersion(binary.BigEndian.Uint32(pubKey.Version())) { + // For BIP-0044 keys, an address type must be specified as we intend to + // not support importing BIP-0044 keys into the wallet using the legacy + // pay-to-pubkey-hash (P2PKH) scheme. A nested witness address type will + // force the standard BIP-0049 derivation scheme (nested witness pubkeys + // everywhere), while a witness address type will force the standard + // BIP-0084 derivation scheme. + case waddrmgr.HDVersionMainNetBIP0044, waddrmgr.HDVersionTestNetBIP0044: + if addrType == nil { + return waddrmgr.KeyScope{}, nil, errors.New("address " + + "type must be specified for account public " + + "key with legacy version") + } + + switch *addrType { + case waddrmgr.NestedWitnessPubKey: + return waddrmgr.KeyScopeBIP0049Plus, + &waddrmgr.KeyScopeBIP0049AddrSchema, nil + + case waddrmgr.WitnessPubKey: + return waddrmgr.KeyScopeBIP0084, nil, nil + + default: + return waddrmgr.KeyScope{}, nil, + fmt.Errorf("unsupported address type %v", + *addrType) + } + + // For BIP-0049 keys, we'll need to make a distinction between the + // traditional BIP-0049 address schema (nested witness pubkeys + // everywhere) and our own BIP-0049Plus address schema (nested + // externally, witness internally). + case waddrmgr.HDVersionMainNetBIP0049, waddrmgr.HDVersionTestNetBIP0049: + if addrType == nil { + return waddrmgr.KeyScope{}, nil, errors.New("address " + + "type must be specified for account public " + + "key with BIP-0049 version") + } + + switch *addrType { + case waddrmgr.NestedWitnessPubKey: + return waddrmgr.KeyScopeBIP0049Plus, + &waddrmgr.KeyScopeBIP0049AddrSchema, nil + + case waddrmgr.WitnessPubKey: + return waddrmgr.KeyScopeBIP0049Plus, nil, nil + + default: + return waddrmgr.KeyScope{}, nil, + fmt.Errorf("unsupported address type %v", + *addrType) + } + + case waddrmgr.HDVersionMainNetBIP0084, waddrmgr.HDVersionTestNetBIP0084: + if addrType != nil && *addrType != waddrmgr.WitnessPubKey { + return waddrmgr.KeyScope{}, nil, + errors.New("address type mismatch") + } + return waddrmgr.KeyScopeBIP0084, nil, nil + + default: + return waddrmgr.KeyScope{}, nil, fmt.Errorf("unknown version %x", + pubKey.Version()) + } +} + +// isPubKeyForNet determines if the given public key is for the current network +// the wallet is operating under. +func (w *Wallet) isPubKeyForNet(pubKey *hdkeychain.ExtendedKey) bool { + version := waddrmgr.HDVersion(binary.BigEndian.Uint32(pubKey.Version())) + switch w.chainParams.Net { + case wire.MainNet: + return version == waddrmgr.HDVersionMainNetBIP0044 || + version == waddrmgr.HDVersionMainNetBIP0049 || + version == waddrmgr.HDVersionMainNetBIP0084 + + case wire.TestNet, wire.TestNet3: + return version == waddrmgr.HDVersionTestNetBIP0044 || + version == waddrmgr.HDVersionTestNetBIP0049 || + version == waddrmgr.HDVersionTestNetBIP0084 + + default: + return false + } +} + +// validateExtendedPubKey ensures a sane derived public key is provided. +func (w *Wallet) validateExtendedPubKey(pubKey *hdkeychain.ExtendedKey, + isAccountKey bool) error { + + // Private keys are not allowed. + if pubKey.IsPrivate() { + return errors.New("private keys cannot be imported") + } + + // The public key must have a version corresponding to the current + // chain. + if !w.isPubKeyForNet(pubKey) { + return fmt.Errorf("expected extended public key for current "+ + "network %v", w.chainParams.Name) + } + + // Verify the extended public key's depth and child index based on + // whether it's an account key or not. + if isAccountKey { + if pubKey.Depth() != accountPubKeyDepth { + return errors.New("invalid account key, must be of the " + + "form m/purpose'/coin_type'/account'") + } + if pubKey.ChildIndex() < hdkeychain.HardenedKeyStart { + return errors.New("invalid account key, must be hardened") + } + } else { + if pubKey.Depth() != pubKeyDepth { + return errors.New("invalid account key, must be of the " + + "form m/purpose'/coin_type'/account'/change/" + + "address_index") + } + if pubKey.ChildIndex() >= hdkeychain.HardenedKeyStart { + return errors.New("invalid pulic key, must not be " + + "hardened") + } + } + + return nil +} + +// ImportAccount imports an account backed by an account extended public key. +// 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. +// +// The address type can usually be inferred from the key's version, but may be +// required for certain keys to map them into the proper scope. +// +// For BIP-0044 keys, an address type must be specified as we intend to not +// support importing BIP-0044 keys into the wallet using the legacy +// pay-to-pubkey-hash (P2PKH) scheme. A nested witness address type will force +// the standard BIP-0049 derivation scheme, while a witness address type will +// force the standard BIP-0084 derivation scheme. +// +// For BIP-0049 keys, an address type must also be specified to make a +// distinction between the traditional BIP-0049 address schema (nested witness +// pubkeys everywhere) and our own BIP-0049Plus address schema (nested +// externally, witness internally). +func (w *Wallet) ImportAccount(name string, accountPubKey *hdkeychain.ExtendedKey, + masterKeyFingerprint uint32, addrType *waddrmgr.AddressType) ( + *waddrmgr.AccountProperties, error) { + + // Ensure we have a valid account public key. + if err := w.validateExtendedPubKey(accountPubKey, true); err != nil { + return nil, err + } + + // Determine what key scope the account public key should belong to and + // whether it should use a custom address schema. + keyScope, addrSchema, err := keyScopeFromPubKey(accountPubKey, addrType) + if err != nil { + return nil, err + } + scopedMgr, err := w.Manager.FetchScopedKeyManager(keyScope) + if err != nil { + return nil, err + } + + // Store the account as watch-only within the database. + var accountProps *waddrmgr.AccountProperties + err = walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error { + ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) + account, err := scopedMgr.NewAccountWatchingOnly( + ns, name, accountPubKey, masterKeyFingerprint, + addrSchema, + ) + if err != nil { + return err + } + accountProps, err = scopedMgr.AccountProperties(ns, account) + return err + }) + return accountProps, err +} + +// ImportPublicKey imports a single derived public key into the address manager. +// The address type can usually be inferred from the key's version, but in the +// case of legacy versions (xpub, tpub), an address type must be specified as we +// intend to not support importing BIP-44 keys into the wallet using the legacy +// pay-to-pubkey-hash (P2PKH) scheme. +func (w *Wallet) ImportPublicKey(pubKey *btcec.PublicKey, + addrType waddrmgr.AddressType) error { + + // Determine what key scope the public key should belong to and import + // it into the key scope's default imported account. + var keyScope waddrmgr.KeyScope + switch addrType { + case waddrmgr.NestedWitnessPubKey: + keyScope = waddrmgr.KeyScopeBIP0049Plus + case waddrmgr.WitnessPubKey: + keyScope = waddrmgr.KeyScopeBIP0084 + default: + return fmt.Errorf("address type %v is not supported", addrType) + } + + scopedKeyManager, err := w.Manager.FetchScopedKeyManager(keyScope) + if err != nil { + return err + } + + // TODO: Perform rescan if requested. + var addr waddrmgr.ManagedAddress + err = walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error { + ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) + addr, err = scopedKeyManager.ImportPublicKey(ns, pubKey, nil) + return err + }) + if err != nil { + return err + } + + log.Infof("Imported address %v", addr.Address()) + + err = w.chainClient.NotifyReceived([]btcutil.Address{addr.Address()}) + if err != nil { + return fmt.Errorf("unable to subscribe for address "+ + "notifications: %v", err) + } + + return nil +} + // ImportPrivateKey imports a private key to the wallet and writes the new // wallet to disk. // diff --git a/wallet/import_test.go b/wallet/import_test.go new file mode 100644 index 0000000..517ff97 --- /dev/null +++ b/wallet/import_test.go @@ -0,0 +1,282 @@ +package wallet + +import ( + "encoding/binary" + "strings" + "testing" + + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcutil" + "github.com/btcsuite/btcutil/hdkeychain" + "github.com/btcsuite/btcwallet/waddrmgr" + "github.com/stretchr/testify/require" +) + +func hardenedKey(key uint32) uint32 { + return key + hdkeychain.HardenedKeyStart +} + +func deriveAcctPubKey(t *testing.T, root *hdkeychain.ExtendedKey, + scope waddrmgr.KeyScope, paths ...uint32) *hdkeychain.ExtendedKey { + + path := []uint32{hardenedKey(scope.Purpose), hardenedKey(scope.Coin)} + path = append(path, paths...) + + var ( + currentKey = root + err error + ) + for _, pathPart := range path { + currentKey, err = currentKey.Derive(pathPart) + require.NoError(t, err) + } + + // The Neuter() method checks the version and doesn't know any + // non-standard methods. We need to convert them to standard, neuter, + // then convert them back with the target extended public key version. + pubVersionBytes := make([]byte, 4) + copy(pubVersionBytes, chaincfg.TestNet3Params.HDPublicKeyID[:]) + switch { + case strings.HasPrefix(root.String(), "uprv"): + binary.BigEndian.PutUint32(pubVersionBytes, uint32( + waddrmgr.HDVersionTestNetBIP0049, + )) + + case strings.HasPrefix(root.String(), "vprv"): + binary.BigEndian.PutUint32(pubVersionBytes, uint32( + waddrmgr.HDVersionTestNetBIP0084, + )) + } + + currentKey, err = currentKey.CloneWithVersion( + chaincfg.TestNet3Params.HDPrivateKeyID[:], + ) + require.NoError(t, err) + currentKey, err = currentKey.Neuter() + require.NoError(t, err) + currentKey, err = currentKey.CloneWithVersion(pubVersionBytes) + require.NoError(t, err) + + return currentKey +} + +type testCase struct { + name string + masterPriv string + accountIndex uint32 + addrType waddrmgr.AddressType + addrSchemaOverride *waddrmgr.ScopeAddrSchema + expectedScope waddrmgr.KeyScope + expectedAddr string + expectedChangeAddr string +} + +var ( + testCases = []*testCase{{ + name: "bip44 with nested witness address type", + masterPriv: "tprv8ZgxMBicQKsPeWwrFuNjEGTTDSY4mRLwd2KDJAPGa1AY" + + "quw38bZqNMSuB3V1Va3hqJBo9Pt8Sx7kBQer5cNMrb8SYquoWPt9" + + "Y3BZdhdtUcw", + accountIndex: 0, + addrType: waddrmgr.NestedWitnessPubKey, + expectedScope: waddrmgr.KeyScopeBIP0049Plus, + expectedAddr: "2N5YTxG9XtGXx1YyhZb7N2pwEjoZLLMHGKj", + expectedChangeAddr: "2N7wpz5Gy2zEJTvq2MAuU6BCTEBLXNQ8dUw", + }, { + name: "bip44 with witness address type", + masterPriv: "tprv8ZgxMBicQKsPeWwrFuNjEGTTDSY4mRLwd2KDJAPGa1AY" + + "quw38bZqNMSuB3V1Va3hqJBo9Pt8Sx7kBQer5cNMrb8SYquoWPt9" + + "Y3BZdhdtUcw", + accountIndex: 777, + addrType: waddrmgr.WitnessPubKey, + expectedScope: waddrmgr.KeyScopeBIP0084, + expectedAddr: "tb1qllxcutkzsukf8u8c8stkp464j0esu9xq7qju8x", + expectedChangeAddr: "tb1qu6jmqglrthscptjqj3egx54wy8xqvzn5hslgw7", + }, { + name: "traditional bip49", + masterPriv: "uprv8tXDerPXZ1QsVp8y6GAMSMYxPQgWi3LSY8qS5ZH9x1YRu" + + "1kGPFjPzR73CFSbVUhdEwJbtsUgucUJ4hGQoJnNepp3RBcE6Jhdom" + + "FD2KeY6G9", + accountIndex: 9, + addrType: waddrmgr.NestedWitnessPubKey, + addrSchemaOverride: &waddrmgr.KeyScopeBIP0049AddrSchema, + expectedScope: waddrmgr.KeyScopeBIP0049Plus, + expectedAddr: "2NBCJ9WzGXZqpLpXGq3Hacybj3c4eHRcqgh", + expectedChangeAddr: "2N3bankFu6F3ZNU41iVJQqyS9MXqp9dvn1M", + }, { + name: "bip49+", + masterPriv: "uprv8tXDerPXZ1QsVp8y6GAMSMYxPQgWi3LSY8qS5ZH9x1YRu" + + "1kGPFjPzR73CFSbVUhdEwJbtsUgucUJ4hGQoJnNepp3RBcE6Jhdom" + + "FD2KeY6G9", + accountIndex: 9, + addrType: waddrmgr.WitnessPubKey, + expectedScope: waddrmgr.KeyScopeBIP0049Plus, + expectedAddr: "2NBCJ9WzGXZqpLpXGq3Hacybj3c4eHRcqgh", + expectedChangeAddr: "tb1qeqn05w2hfq6axpdprhs4y7x65gxkkvfvyxqk4u", + }, { + name: "bip84", + masterPriv: "vprv9DMUxX4ShgxMM7L5vcwyeSeTZNpxefKwTFMerxB3L1vJ" + + "x7ZVdutxcUmBDTQBVPMYeaRQeM5FNGpqwysyX1CPT4VeHXJegDX8" + + "5VJrQvaFaz3", + accountIndex: 1, + addrType: waddrmgr.WitnessPubKey, + expectedScope: waddrmgr.KeyScopeBIP0084, + expectedAddr: "tb1q5vepvcl0z8xj7kps4rsux722r4dvfwlhk6j532", + expectedChangeAddr: "tb1qlwe2kgxcsa8x4huu79yff4rze0l5mwafg5c7xd", + }} +) + +// TestImportAccount tests that extended public keys can successfully be +// imported into both watch only and normal wallets. +func TestImportAccount(t *testing.T) { + for _, tc := range testCases { + tc := tc + + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + w, cleanup := testWallet(t) + defer cleanup() + + testImportAccount(t, w, tc, false, tc.name) + }) + + name := tc.name + " watch-only" + t.Run(name, func(t *testing.T) { + t.Parallel() + + w, cleanup := testWalletWatchingOnly(t) + defer cleanup() + + testImportAccount(t, w, tc, true, name) + }) + } +} + +func testImportAccount(t *testing.T, w *Wallet, tc *testCase, watchOnly bool, + name string) { + + // First derive the master public key of the account we want to import. + root, err := hdkeychain.NewKeyFromString(tc.masterPriv) + require.NoError(t, err) + + // Derive the extended private and public key for our target account. + acct1Pub := deriveAcctPubKey( + t, root, tc.expectedScope, hardenedKey(tc.accountIndex), + ) + + // We want to make sure we can import and handle multiple accounts, so + // we create another one. + acct2Pub := deriveAcctPubKey( + t, root, tc.expectedScope, hardenedKey(tc.accountIndex+1), + ) + + // And we also want to be able to import loose extended public keys + // without needing to specify an explicit scope. + acct3ExternalExtPub := deriveAcctPubKey( + t, root, tc.expectedScope, hardenedKey(tc.accountIndex+2), 0, 0, + ) + acct3ExternalPub, err := acct3ExternalExtPub.ECPubKey() + require.NoError(t, err) + + // Import the extended public keys into new accounts. + acct1, err := w.ImportAccount( + name+"1", acct1Pub, root.ParentFingerprint(), &tc.addrType, + ) + require.NoError(t, err) + require.Equal(t, tc.expectedScope, acct1.KeyScope) + + acct2, err := w.ImportAccount( + name+"2", acct2Pub, root.ParentFingerprint(), &tc.addrType, + ) + require.NoError(t, err) + require.Equal(t, tc.expectedScope, acct2.KeyScope) + + err = w.ImportPublicKey(acct3ExternalPub, tc.addrType) + require.NoError(t, err) + + // If the wallet is watch only, there is no default account and our + // imported account will be index 0. + firstAccountIndex := uint32(1) + if watchOnly { + firstAccountIndex = 0 + } + + // We should have 3 additional accounts now. + acctResult, err := w.Accounts(tc.expectedScope) + require.NoError(t, err) + require.Len(t, acctResult.Accounts, int(firstAccountIndex*2)+2) + + // Validate the state of the accounts. + require.Equal(t, firstAccountIndex, acct1.AccountNumber) + require.Equal(t, name+"1", acct1.AccountName) + require.Equal(t, true, acct1.IsWatchOnly) + require.Equal(t, root.ParentFingerprint(), acct1.MasterKeyFingerprint) + require.NotNil(t, acct1.AccountPubKey) + require.Equal(t, acct1Pub.String(), acct1.AccountPubKey.String()) + require.Equal(t, uint32(0), acct1.InternalKeyCount) + require.Equal(t, uint32(0), acct1.ExternalKeyCount) + require.Equal(t, uint32(0), acct1.ImportedKeyCount) + + require.Equal(t, firstAccountIndex+1, acct2.AccountNumber) + require.Equal(t, name+"2", acct2.AccountName) + require.Equal(t, true, acct2.IsWatchOnly) + require.Equal(t, root.ParentFingerprint(), acct2.MasterKeyFingerprint) + require.NotNil(t, acct2.AccountPubKey) + require.Equal(t, acct2Pub.String(), acct2.AccountPubKey.String()) + require.Equal(t, uint32(0), acct2.InternalKeyCount) + require.Equal(t, uint32(0), acct2.ExternalKeyCount) + require.Equal(t, uint32(0), acct2.ImportedKeyCount) + + // Test address derivation. + addr, err := w.NewAddress(acct1.AccountNumber, tc.expectedScope) + require.NoError(t, err) + require.Equal(t, tc.expectedAddr, addr.String()) + addr, err = w.NewChangeAddress(acct1.AccountNumber, tc.expectedScope) + require.NoError(t, err) + require.Equal(t, tc.expectedChangeAddr, addr.String()) + + // Make sure the key count was increased. + acct1, err = w.AccountProperties(tc.expectedScope, acct1.AccountNumber) + require.NoError(t, err) + require.Equal(t, uint32(1), acct1.InternalKeyCount) + require.Equal(t, uint32(1), acct1.ExternalKeyCount) + require.Equal(t, uint32(0), acct1.ImportedKeyCount) + + // Make sure we can't get private keys for the imported accounts. + _, err = w.DumpWIFPrivateKey(addr) + require.True(t, waddrmgr.IsError(err, waddrmgr.ErrWatchingOnly)) + + // Get the address info for the single key we imported. + switch tc.addrType { + case waddrmgr.NestedWitnessPubKey: + witnessAddr, err := btcutil.NewAddressWitnessPubKeyHash( + btcutil.Hash160(acct3ExternalPub.SerializeCompressed()), + &chaincfg.TestNet3Params, + ) + require.NoError(t, err) + + witnessProg, err := txscript.PayToAddrScript(witnessAddr) + require.NoError(t, err) + + addr, err = btcutil.NewAddressScriptHash( + witnessProg, &chaincfg.TestNet3Params, + ) + require.NoError(t, err) + + case waddrmgr.WitnessPubKey: + addr, err = btcutil.NewAddressWitnessPubKeyHash( + btcutil.Hash160(acct3ExternalPub.SerializeCompressed()), + &chaincfg.TestNet3Params, + ) + require.NoError(t, err) + + default: + t.Fatalf("unhandled address type %v", tc.addrType) + } + + addrManaged, err := w.AddressInfo(addr) + require.NoError(t, err) + require.Equal(t, true, addrManaged.Imported()) +} From f1b329f680b9bd446ec8240ecc3a331babcb0bec Mon Sep 17 00:00:00 2001 From: Wilmer Paulino Date: Wed, 10 Mar 2021 18:11:16 -0800 Subject: [PATCH 16/16] waddrmgr: add TODO to determine proper address type of imported key For key scopes which have an address schema where the external and internal branches differ, we always assume that imported keys use the external address type defined in the scope's address schema. This may not always be the case however, and should be handled correctly. Ideally, we generate two addresses per imported key (only if the external and internal address types differ) and scan for both in the chain. --- waddrmgr/scoped_manager.go | 65 ++++++++++++++++---------------------- 1 file changed, 28 insertions(+), 37 deletions(-) diff --git a/waddrmgr/scoped_manager.go b/waddrmgr/scoped_manager.go index 0a6c6cb..b91ccef 100644 --- a/waddrmgr/scoped_manager.go +++ b/waddrmgr/scoped_manager.go @@ -177,6 +177,13 @@ var ( ExternalAddrType: NestedWitnessPubKey, InternalAddrType: NestedWitnessPubKey, } + + // ImportedDerivationPath is the derivation path for an imported + // address. The Account, Branch, and Index members are not known, so + // they are left blank. + ImportedDerivationPath = DerivationPath{ + InternalAccount: ImportedAddrAccount, + } ) // ScopedKeyManager is a sub key manager under the main root key manager. The @@ -665,15 +672,10 @@ func (s *ScopedKeyManager) importedAddressRowToManaged(row *dbImportedAddressRow return nil, managerError(ErrCrypto, str, err) } - // Since this is an imported address, we won't populate the full - // derivation path, as we don't have enough information to do so. - derivationPath := DerivationPath{ - InternalAccount: row.account, - } - + // TODO: Handle imported key being part of internal branch. compressed := len(pubBytes) == btcec.PubKeyBytesLenCompressed ma, err := newManagedAddressWithoutPrivKey( - s, derivationPath, pubKey, compressed, + s, ImportedDerivationPath, pubKey, compressed, s.addrSchema.ExternalAddrType, ) if err != nil { @@ -1753,22 +1755,12 @@ func (s *ScopedKeyManager) ImportPrivateKey(ns walletdb.ReadWriteBucket, return nil, err } - // The full derivation path for an imported key is incomplete as we - // don't know exactly how it was derived. - importedDerivationPath := DerivationPath{ - InternalAccount: ImportedAddrAccount, - } - // Create a new managed address based on the imported address. if !s.rootManager.WatchOnly() { - return s.toPrivateManagedAddress( - wif, true, importedDerivationPath, - ) + return s.toImportedPrivateManagedAddress(wif) } pubKey := (*btcec.PublicKey)(&wif.PrivKey.PublicKey) - return s.toPublicManagedAddress( - pubKey, wif.CompressPubKey, true, importedDerivationPath, - ) + return s.toImportedPublicManagedAddress(pubKey, wif.CompressPubKey) } // ImportPublicKey imports a public key into the address manager. @@ -1789,14 +1781,7 @@ func (s *ScopedKeyManager) ImportPublicKey(ns walletdb.ReadWriteBucket, return nil, err } - // The full derivation path for an imported key is incomplete as we - // don't know exactly how it was derived. - importedDerivationPath := DerivationPath{ - InternalAccount: ImportedAddrAccount, - } - return s.toPublicManagedAddress( - pubKey, true, true, importedDerivationPath, - ) + return s.toImportedPublicManagedAddress(pubKey, true) } // importPublicKey imports a public key into the address manager and updates the @@ -1883,19 +1868,22 @@ func (s *ScopedKeyManager) importPublicKey(ns walletdb.ReadWriteBucket, return nil } -// toPrivateManagedAddress converts a private key to a managed address. -func (s *ScopedKeyManager) toPrivateManagedAddress(wif *btcutil.WIF, - imported bool, derivationPath DerivationPath) (*managedAddress, error) { +// toImportedPrivateManagedAddress converts an imported private key to an +// imported managed address. +func (s *ScopedKeyManager) toImportedPrivateManagedAddress( + wif *btcutil.WIF) (*managedAddress, error) { // Create a new managed address based on the imported address. + // + // TODO: Handle imported key being part of internal branch. managedAddr, err := newManagedAddress( - s, derivationPath, wif.PrivKey, wif.CompressPubKey, + s, ImportedDerivationPath, wif.PrivKey, wif.CompressPubKey, s.addrSchema.ExternalAddrType, ) if err != nil { return nil, err } - managedAddr.imported = imported + managedAddr.imported = true // Add the new managed address to the cache of recent addresses and // return it. @@ -1903,19 +1891,22 @@ func (s *ScopedKeyManager) toPrivateManagedAddress(wif *btcutil.WIF, return managedAddr, nil } -// toPublicManagedAddress converts a public key to a managed address. -func (s *ScopedKeyManager) toPublicManagedAddress(pubKey *btcec.PublicKey, - compressed, imported bool, derivationPath DerivationPath) (*managedAddress, error) { +// toPublicManagedAddress converts an imported public key to an imported managed +// address. +func (s *ScopedKeyManager) toImportedPublicManagedAddress( + pubKey *btcec.PublicKey, compressed bool) (*managedAddress, error) { // Create a new managed address based on the imported address. + // + // TODO: Handle imported key being part of internal branch. managedAddr, err := newManagedAddressWithoutPrivKey( - s, derivationPath, pubKey, compressed, + s, ImportedDerivationPath, pubKey, compressed, s.addrSchema.ExternalAddrType, ) if err != nil { return nil, err } - managedAddr.imported = imported + managedAddr.imported = true // Add the new managed address to the cache of recent addresses and // return it.