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 ./... diff --git a/go.mod b/go.mod index b9d1b4d..82182d9 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,9 @@ 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 + 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 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/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/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/address.go b/waddrmgr/address.go index b8008ae..3763a1c 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 } @@ -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 { @@ -177,11 +183,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 +551,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/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 4dd4cae..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. @@ -160,16 +162,63 @@ 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 + + // 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 @@ -1178,9 +1227,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() @@ -1372,13 +1421,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 +1454,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 +1471,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 } @@ -1672,7 +1727,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, ) @@ -1680,7 +1735,7 @@ func createManagerKeyScope(ns walletdb.ReadWriteBucket, return err } - return putAccountInfo( + return putDefaultAccountInfo( ns, &scope, ImportedAddrAccount, nil, nil, 0, 0, ImportedAddrAccountName, ) @@ -1783,10 +1838,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 { diff --git a/waddrmgr/manager_test.go b/waddrmgr/manager_test.go index c4667ad..d574302 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 @@ -1939,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 { @@ -1951,14 +1960,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 +1990,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) @@ -2640,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) @@ -2705,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) @@ -2743,9 +2756,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 44f9b95..b91ccef 100644 --- a/waddrmgr/scoped_manager.go +++ b/waddrmgr/scoped_manager.go @@ -1,17 +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 @@ -22,6 +55,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 @@ -66,7 +103,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) } @@ -131,6 +168,22 @@ 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, + } + + // 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 @@ -212,20 +265,14 @@ 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, acctInfo *accountInfo) ( + ManagedAddress, error) { - var addrType AddressType - if branch == InternalBranch { - addrType = s.addrSchema.InternalAddrType - } else { - addrType = s.addrSchema.ExternalAddrType - } - - derivationPath := DerivationPath{ - Account: account, - Branch: branch, - Index: index, - } + // 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 @@ -244,13 +291,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 } @@ -271,14 +318,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 -- "+ @@ -310,86 +357,124 @@ 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, - } - - if !s.rootManager.isLocked() { - // 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, + acctType: row.acctType, + 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, + acctType: row.acctType, + nextExternalIndex: row.nextExternalIndex, + nextInternalIndex: row.nextInternalIndex, + addrSchema: row.addrSchema, + masterKeyFingerprint: row.masterKeyFingerprint, + } + + // Use the crypto public key to decrypt the account public + // extended key. + acctInfo.acctKeyPub, err = decryptKey( + s.rootManager.cryptoKeyPub, row.pubKeyEncrypted, + ) + if err != nil { + str := fmt.Sprintf("failed to decrypt public key for "+ + "account %d", account) + return nil, managerError(ErrCrypto, str, err) + } + + 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-- } - lastExtKey, err := s.deriveKey( - acctInfo, branch, index, !s.rootManager.isLocked(), - ) + lastExtAddrPath := DerivationPath{ + InternalAccount: account, + Account: acctInfo.acctKeyPub.ChildIndex(), + Branch: branch, + Index: index, + } + lastExtKey, err := s.deriveKey(acctInfo, branch, index, hasPrivateKey) if err != nil { return nil, err } - lastExtAddr, err := s.keyToManaged(lastExtKey, account, branch, index) + lastExtAddr, err := s.keyToManaged(lastExtKey, lastExtAddrPath, acctInfo) if err != nil { return nil, err } 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-- } - lastIntKey, err := s.deriveKey( - acctInfo, branch, index, !s.rootManager.isLocked(), - ) + lastIntAddrPath := DerivationPath{ + InternalAccount: account, + Account: acctInfo.acctKeyPub.ChildIndex(), + Branch: branch, + Index: index, + } + lastIntKey, err := s.deriveKey(acctInfo, branch, index, hasPrivateKey) if err != nil { return nil, err } - lastIntAddr, err := s.keyToManaged(lastIntKey, account, branch, index) + lastIntAddr, err := s.keyToManaged(lastIntKey, lastIntAddrPath, acctInfo) if err != nil { return nil, err } @@ -408,7 +493,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. @@ -429,8 +517,37 @@ 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 + + // 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() // Could be more efficient if this was tracked by the db. var importedKeyCount uint32 @@ -451,36 +568,59 @@ 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) -} - -// deriveKeyFromPath returns either a public or private derived extended key -// based on the private flag for the given an account, branch, and index. -// -// 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) { - - // Look up the account key information. - acctInfo, err := s.loadAccountInfo(ns, account) + acctInfo, err := s.loadAccountInfo(ns, kp.InternalAccount) if err != nil { return nil, err } + return s.keyToManaged(addrKey, kp, acctInfo) +} - return s.deriveKey(acctInfo, branch, index, private) +// deriveKeyFromPath returns either a public or private derived extended key +// 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, + internalAccount, branch, index uint32, private bool) ( + *hdkeychain.ExtendedKey, *hdkeychain.ExtendedKey, error) { + + // Look up the account key information. + acctInfo, err := s.loadAccountInfo(ns, internalAccount) + if err != nil { + return nil, nil, err + } + private = private && acctInfo.acctKeyPriv != nil + + addrKey, err := s.deriveKey(acctInfo, branch, index, private) + if err != nil { + return nil, nil, err + } + + acctKey := acctInfo.acctKeyPub + if private { + acctKey = acctInfo.acctKeyPriv + } + + return addrKey, acctKey, nil } // chainAddressRowToManaged returns a new managed address based on chained @@ -492,16 +632,27 @@ 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) + 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, + ) } // importedAddressRowToManaged returns a new managed address based on imported @@ -521,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{ - Account: 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 { @@ -670,6 +816,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. // @@ -687,7 +850,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 } @@ -699,10 +863,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. @@ -715,7 +879,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 +896,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. @@ -756,9 +920,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 @@ -842,7 +1007,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) } } @@ -882,7 +1047,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 } @@ -894,10 +1060,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. @@ -915,7 +1081,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 +1100,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. @@ -958,9 +1124,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 @@ -1030,7 +1197,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) } } @@ -1209,14 +1376,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() @@ -1224,7 +1401,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 @@ -1333,7 +1512,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 { @@ -1345,8 +1524,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() @@ -1361,7 +1551,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 } @@ -1370,9 +1563,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 { @@ -1391,14 +1594,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 - err = putAccountInfo( - ns, &s.scope, account, acctPubEnc, nil, 0, 0, name, + err = putWatchOnlyAccountInfo( + ns, &s.scope, account, acctPubEnc, masterKeyFingerprint, 0, 0, + name, addrSchema, ) if err != nil { return err @@ -1440,29 +1644,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 @@ -1513,14 +1733,94 @@ 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, + s.addrSchema.ExternalAddrType, bs, + ) + if err != nil { + return nil, err + } + + // Create a new managed address based on the imported address. + if !s.rootManager.WatchOnly() { + return s.toImportedPrivateManagedAddress(wif) + } + pubKey := (*btcec.PublicKey)(&wif.PrivKey.PublicKey) + return s.toImportedPublicManagedAddress(pubKey, wif.CompressPubKey) +} + +// 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 + } + + return s.toImportedPublicManagedAddress(pubKey, true) +} + +// 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, 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. - serializedPubKey := wif.SerializePubKey() - 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) - return nil, managerError(ErrDuplicateAddress, str, nil) + return managerError(ErrDuplicateAddress, str, nil) } // Encrypt public key. @@ -1530,42 +1830,30 @@ 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 // 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 // needed) in a single transaction. err = putImportedAddress( - ns, &s.scope, pubKeyHash, ImportedAddrAccount, ssNone, + ns, &s.scope, addressID, ImportedAddrAccount, ssNone, 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,26 +1865,44 @@ 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 +} + +// 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. - 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, - ) + // + // TODO: Handle imported key being part of internal branch. + managedAddr, err := newManagedAddress( + s, ImportedDerivationPath, wif.PrivKey, wif.CompressPubKey, + s.addrSchema.ExternalAddrType, + ) + if err != nil { + return nil, err } + managedAddr.imported = true + + // 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 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, ImportedDerivationPath, pubKey, compressed, + s.addrSchema.ExternalAddrType, + ) if err != nil { return nil, err } @@ -1627,7 +1933,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) } @@ -1870,3 +2176,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[:]) +} 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 new file mode 100644 index 0000000..ce867ed --- /dev/null +++ b/wallet/import.go @@ -0,0 +1,365 @@ +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. +// +// 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/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()) +} 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 079fda0..8c932da 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, } } @@ -2733,110 +2735,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 { diff --git a/wtxmgr/tx_test.go b/wtxmgr/tx_test.go index 9434845..855dc3b 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.