diff --git a/account.go b/account.go index f056ec9..82b9991 100644 --- a/account.go +++ b/account.go @@ -424,6 +424,7 @@ func (a *Account) ImportWIFPrivateKey(wif string, bs *wallet.BlockStamp) (string a.mtx.Unlock() return "", err } + addrStr := addr.String() // Immediately write dirty wallet to disk. // @@ -438,12 +439,12 @@ func (a *Account) ImportWIFPrivateKey(wif string, bs *wallet.BlockStamp) (string } // Associate the imported address with this account. - MarkAddressForAccount(addr, a.Name()) + MarkAddressForAccount(addrStr, a.Name()) - log.Infof("Imported payment address %v", addr) + log.Infof("Imported payment address %v", addrStr) // Return the payment address string of the imported private key. - return addr, nil + return addrStr, nil } // Track requests btcd to send notifications of new transactions for diff --git a/wallet/wallet.go b/wallet/wallet.go index 943022e..ca70d7a 100644 --- a/wallet/wallet.go +++ b/wallet/wallet.go @@ -53,13 +53,14 @@ const ( // Possible errors when dealing with wallets. var ( - ErrAddressNotFound = errors.New("address not found") - ErrChecksumMismatch = errors.New("checksum mismatch") - ErrDuplicate = errors.New("duplicate key or address") - ErrMalformedEntry = errors.New("malformed entry") - ErrNetworkMismatch = errors.New("network mismatch") - ErrWalletDoesNotExist = errors.New("non-existant wallet") - ErrWalletLocked = errors.New("wallet is locked") + ErrAddressNotFound = errors.New("address not found") + ErrChecksumMismatch = errors.New("checksum mismatch") + ErrDuplicate = errors.New("duplicate key or address") + ErrMalformedEntry = errors.New("malformed entry") + ErrNetworkMismatch = errors.New("network mismatch") + ErrWalletDoesNotExist = errors.New("non-existant wallet") + ErrWalletIsWatchingOnly = errors.New("wallet is watching-only") + ErrWalletLocked = errors.New("wallet is locked") ) var ( @@ -681,7 +682,10 @@ func (w *Wallet) ReadFrom(r io.Reader) (n int64, err error) { // If the private keys have not ben created yet, mark the // earliest so all can be created on next wallet unlock. if e.addr.flags.createPrivKeyNextUnlock { - if w.missingKeysStart < e.addr.chainIndex { + switch { + case w.missingKeysStart == 0: + fallthrough + case e.addr.chainIndex < w.missingKeysStart: w.missingKeysStart = e.addr.chainIndex } } @@ -781,6 +785,10 @@ func (w *Wallet) WriteTo(wtr io.Writer) (n int64, err error) { // addresses created while the wallet was locked without private // keys are created at this time. func (w *Wallet) Unlock(passphrase []byte) error { + if w.flags.watchingOnly { + return ErrWalletIsWatchingOnly + } + // Derive key from KDF parameters and passphrase. key := Key(passphrase, &w.kdfParams) @@ -798,6 +806,10 @@ func (w *Wallet) Unlock(passphrase []byte) error { // Lock performs a best try effort to remove and zero all secret keys // associated with the wallet. func (w *Wallet) Lock() (err error) { + if w.flags.watchingOnly { + return ErrWalletIsWatchingOnly + } + // Remove clear text passphrase from wallet. if len(w.secret) != 32 { err = ErrWalletLocked @@ -872,7 +884,7 @@ func (w *Wallet) NextChainedAddress(bs *BlockStamp, // LastChainedAddress returns the most recently requested chained // address from calling NextChainedAddress, or the root address if // no chained addresses have been requested. -func (w *Wallet) LastChainedAddress() btcutil.Address { +func (w *Wallet) LastChainedAddress() *btcutil.AddressPubKeyHash { return w.chainIdxMap[w.highestUsed] } @@ -1018,6 +1030,11 @@ func (w *Wallet) createMissingPrivateKeys() error { // contained in the wallet, the address does not include a public and // private key, or if the wallet is locked. func (w *Wallet) AddressKey(a btcutil.Address) (key *ecdsa.PrivateKey, err error) { + // Watching-only wallets do not contain private keys. + if w.flags.watchingOnly { + return nil, ErrWalletIsWatchingOnly + } + // Currently, only P2PKH addresses are supported. This should // be extended to a switch-case statement when support for other // addresses are added. @@ -1187,36 +1204,39 @@ func (w *Wallet) SetBetterEarliestBlockHeight(height int32) { } // ImportPrivateKey creates a new encrypted btcAddress with a -// user-provided private key and adds it to the wallet. If the -// import is successful, the payment address string is returned. -func (w *Wallet) ImportPrivateKey(privkey []byte, compressed bool, bs *BlockStamp) (string, error) { +// user-provided private key and adds it to the wallet. +func (w *Wallet) ImportPrivateKey(privkey []byte, compressed bool, bs *BlockStamp) (*btcutil.AddressPubKeyHash, error) { + if w.flags.watchingOnly { + return nil, ErrWalletIsWatchingOnly + } + // First, must check that the key being imported will not result // in a duplicate address. pkh := btcutil.Hash160(pubkeyFromPrivkey(privkey, compressed)) // Will always be valid inputs so omit error check. apkh, err := btcutil.NewAddressPubKeyHash(pkh, w.Net()) if err != nil { - return "", err + return nil, err } if _, ok := w.addrMap[*apkh]; ok { - return "", ErrDuplicate + return nil, ErrDuplicate } // The wallet must be unlocked to encrypt the imported private key. if len(w.secret) != 32 { - return "", ErrWalletLocked + return nil, ErrWalletLocked } // Create new address with this private key. btcaddr, err := newBtcAddress(privkey, nil, bs, compressed) if err != nil { - return "", err + return nil, err } btcaddr.chainIndex = importedKeyChainIdx // Encrypt imported address with the derived AES key. if err = btcaddr.encrypt(w.secret); err != nil { - return "", err + return nil, err } // Add address to wallet's bookkeeping structures. Adding to @@ -1225,11 +1245,8 @@ func (w *Wallet) ImportPrivateKey(privkey []byte, compressed bool, bs *BlockStam w.addrMap[*btcaddr.address(w.net)] = btcaddr w.importedAddrs = append(w.importedAddrs, btcaddr) - // Create and return encoded payment address string. Error is - // ignored as the length of the pubkey hash and net will always - // be valid. - addr, _ := btcutil.NewAddressPubKeyHash(btcaddr.pubKeyHash[:], w.Net()) - return addr.String(), nil + // Create and return address. + return btcutil.NewAddressPubKeyHash(btcaddr.pubKeyHash[:], w.Net()) } // CreateDate returns the Unix time of the wallet creation time. This @@ -1239,6 +1256,71 @@ func (w *Wallet) CreateDate() int64 { return w.createDate } +// ExportWatchingWallet creates and returns a new wallet with the same +// addresses in w, but as a watching-only wallet without any private keys. +// New addresses created by the watching wallet will match the new addresses +// created the original wallet (thanks to public key address chaining), but +// will be missing the associated private keys. +func (w *Wallet) ExportWatchingWallet() (*Wallet, error) { + // Don't continue if wallet is already a watching-only wallet. + if w.flags.watchingOnly { + return nil, ErrWalletIsWatchingOnly + } + + // Copy members of w into a new wallet, but mark as watching-only and + // do not include any private keys. + ww := &Wallet{ + net: w.net, + flags: walletFlags{ + useEncryption: false, + watchingOnly: true, + }, + uniqID: w.uniqID, + name: w.name, + desc: w.desc, + createDate: w.createDate, + highestUsed: w.highestUsed, + keyGenerator: *w.keyGenerator.watchingCopy(), + recent: recentBlocks{ + lastHeight: w.recent.lastHeight, + }, + + addrMap: make(map[btcutil.AddressPubKeyHash]*btcAddress), + addrCommentMap: make(map[btcutil.AddressPubKeyHash]comment), + txCommentMap: make(map[transactionHashKey]comment), + + chainIdxMap: make(map[int64]*btcutil.AddressPubKeyHash), + lastChainIdx: w.lastChainIdx, + } + + if len(w.recent.hashes) != 0 { + ww.recent.hashes = make([]*btcwire.ShaHash, 0, len(w.recent.hashes)) + for _, hash := range w.recent.hashes { + var hashCpy btcwire.ShaHash + copy(hashCpy[:], hash[:]) + ww.recent.hashes = append(ww.recent.hashes, &hashCpy) + } + } + for apkh, addr := range w.addrMap { + apkhCopy := apkh + ww.chainIdxMap[addr.chainIndex] = &apkhCopy + ww.addrMap[apkhCopy] = addr.watchingCopy() + } + for apkh, cmt := range w.addrCommentMap { + cmtCopy := make(comment, len(cmt)) + copy(cmtCopy, cmt) + ww.addrCommentMap[apkh] = cmtCopy + } + if len(w.importedAddrs) != 0 { + ww.importedAddrs = make([]*btcAddress, 0, len(w.importedAddrs)) + for _, addr := range w.importedAddrs { + ww.importedAddrs = append(ww.importedAddrs, addr.watchingCopy()) + } + } + + return ww, nil +} + // AddressInfo holds information regarding an address needed to manage // a complete wallet. type AddressInfo struct { @@ -1299,30 +1381,36 @@ type walletFlags struct { watchingOnly bool } -func (wf *walletFlags) ReadFrom(r io.Reader) (n int64, err error) { - raw := make([]byte, 8) - n, err = binaryRead(r, binary.LittleEndian, raw) - wf.useEncryption = raw[0] != 0 - wf.watchingOnly = raw[1] != 0 - return n, err +func (wf *walletFlags) ReadFrom(r io.Reader) (int64, error) { + var b [8]byte + n, err := r.Read(b[:]) + if err != nil { + return int64(n), err + } + + wf.useEncryption = b[0]&(1<<0) != 0 + wf.watchingOnly = b[0]&(1<<1) != 0 + + return int64(n), nil } -func (wf *walletFlags) WriteTo(w io.Writer) (n int64, err error) { - raw := make([]byte, 8) +func (wf *walletFlags) WriteTo(w io.Writer) (int64, error) { + var b [8]byte if wf.useEncryption { - raw[0] = 1 + b[0] |= 1 << 0 } if wf.watchingOnly { - raw[1] = 1 + b[0] |= 1 << 1 } - return binaryWrite(w, binary.LittleEndian, raw) + n, err := w.Write(b[:]) + return int64(n), err } type addrFlags struct { hasPrivKey bool hasPubKey bool encrypted bool - createPrivKeyNextUnlock bool // unimplemented in btcwallet + createPrivKeyNextUnlock bool compressed bool } @@ -1640,7 +1728,7 @@ type btcAddress struct { flags addrFlags chaincode [32]byte chainIndex int64 - chainDepth int64 // currently unused (will use when extending a locked wallet) + chainDepth int64 // unused initVector [16]byte privKey [32]byte pubKey publicKey @@ -2053,6 +2141,30 @@ func (a *btcAddress) info(net btcwire.BitcoinNet) (*AddressInfo, error) { }, nil } +// watchingCopy creates a copy of an address without a private key. +// This is used to fill a watching a wallet with addresses from a +// normal wallet. +func (a *btcAddress) watchingCopy() *btcAddress { + return &btcAddress{ + pubKeyHash: a.pubKeyHash, + flags: addrFlags{ + hasPrivKey: false, + hasPubKey: a.flags.hasPubKey, + encrypted: false, + createPrivKeyNextUnlock: false, + compressed: a.flags.compressed, + }, + chaincode: a.chaincode, + chainIndex: a.chainIndex, + chainDepth: a.chainDepth, + pubKey: a.pubKey, + firstSeen: a.firstSeen, + lastSeen: a.lastSeen, + firstBlock: a.firstBlock, + lastBlock: a.lastBlock, + } +} + func walletHash(b []byte) uint32 { sum := btcwire.DoubleSha256(b) return binary.LittleEndian.Uint32(sum) diff --git a/wallet/wallet_test.go b/wallet/wallet_test.go index 61c6e3d..001d899 100644 --- a/wallet/wallet_test.go +++ b/wallet/wallet_test.go @@ -22,6 +22,7 @@ import ( "crypto/rand" "encoding/hex" "github.com/conformal/btcec" + "github.com/conformal/btcutil" "github.com/conformal/btcwire" "github.com/davecgh/go-spew/spew" "math/big" @@ -462,3 +463,161 @@ func TestWalletPubkeyChaining(t *testing.T) { return } } + +func TestWatchingWalletExport(t *testing.T) { + const keypoolSize = 10 + createdAt := &BlockStamp{} + w, err := NewWallet("banana wallet", "A wallet for testing.", + []byte("banana"), btcwire.MainNet, createdAt, keypoolSize) + if err != nil { + t.Error("Error creating new wallet: " + err.Error()) + return + } + + // Maintain a set of the active addresses in the wallet. + activeAddrs := make(map[btcutil.AddressPubKeyHash]struct{}) + + // Add root address. + activeAddrs[*w.LastChainedAddress()] = struct{}{} + + // Get as many new active addresses as necessary to deplete the keypool. + // This is done as we will want to test that new addresses created by + // the watching wallet do not pull from previous public keys in the + // original keypool. + for i := 0; i < keypoolSize; i++ { + apkh, err := w.NextChainedAddress(createdAt, keypoolSize) + if err != nil { + t.Errorf("unable to get next address: %v", err) + return + } + activeAddrs[*apkh] = struct{}{} + } + + // Create watching wallet from w. + ww, err := w.ExportWatchingWallet() + if err != nil { + t.Errorf("Could not create watching wallet: %v", err) + return + } + + // Verify correctness of wallet flags. + if ww.flags.useEncryption { + t.Errorf("Watching wallet marked as using encryption (but nothing to encrypt).") + return + } + if !ww.flags.watchingOnly { + t.Errorf("Wallet should be watching-only but is not marked so.") + return + } + + // Verify that all flags are set as expected. + if ww.keyGenerator.flags.encrypted { + t.Errorf("Watching root address should not be encrypted (nothing to encrypt)") + return + } + if ww.keyGenerator.flags.hasPrivKey { + t.Errorf("Watching root address marked as having a private key.") + return + } + if !ww.keyGenerator.flags.hasPubKey { + t.Errorf("Watching root address marked as missing a public key.") + return + } + if ww.keyGenerator.flags.createPrivKeyNextUnlock { + t.Errorf("Watching root address marked as needing a private key to be generated later.") + return + } + for apkh, addr := range ww.addrMap { + if addr.flags.encrypted { + t.Errorf("Chained address should not be encrypted (nothing to encrypt)") + return + } + if ww.keyGenerator.flags.hasPrivKey { + t.Errorf("Chained address marked as having a private key.") + return + } + if !ww.keyGenerator.flags.hasPubKey { + t.Errorf("Chained address marked as missing a public key.") + return + } + if ww.keyGenerator.flags.createPrivKeyNextUnlock { + t.Errorf("Chained address marked as needing a private key to be generated later.") + return + } + + if _, ok := activeAddrs[apkh]; !ok { + t.Errorf("Address from watching wallet not found in original wallet.") + return + } + delete(activeAddrs, apkh) + } + if len(activeAddrs) != 0 { + t.Errorf("%v address(es) were not exported to watching wallet.", len(activeAddrs)) + return + } + + // Check that the new addresses created by each wallet match. The + // original wallet is unlocked so the keypool is refilled and chained + // addresses use the previous' privkey, not pubkey. + if err := w.Unlock([]byte("banana")); err != nil { + t.Errorf("Unlocking original wallet failed: %v", err) + } + for i := 0; i < keypoolSize; i++ { + addr, err := w.NextChainedAddress(createdAt, keypoolSize) + if err != nil { + t.Errorf("Cannot get next chained address for original wallet: %v", err) + return + } + waddr, err := ww.NextChainedAddress(createdAt, keypoolSize) + if err != nil { + t.Errorf("Cannot get next chained address for watching wallet: %v", err) + return + } + if addr.String() != waddr.String() { + t.Errorf("Next addresses for each wallet do not match eachother.") + return + } + } + + // Test (de)serialization of watching wallet. + buf := new(bytes.Buffer) + _, err = ww.WriteTo(buf) + if err != nil { + t.Errorf("Cannot write watching wallet: %v", err) + return + } + ww2 := new(Wallet) + _, err = ww2.ReadFrom(buf) + if err != nil { + t.Errorf("Cannot read watching wallet: %v", err) + return + } + + // Check that (de)serialized watching wallet matches the exported wallet. + if !reflect.DeepEqual(ww, ww2) { + t.Error("Exported and read-in watching wallets do not match.") + return + } + + // Verify that nonsensical functions fail with correct error. + if err := ww.Lock(); err != ErrWalletIsWatchingOnly { + t.Errorf("Nonsensical func Lock returned no or incorrect error: %v", err) + return + } + if err := ww.Unlock([]byte("banana")); err != ErrWalletIsWatchingOnly { + t.Errorf("Nonsensical func Unlock returned no or incorrect error: %v", err) + return + } + if _, err := ww.AddressKey(w.keyGenerator.address(ww.net)); err != ErrWalletIsWatchingOnly { + t.Errorf("Nonsensical func AddressKey returned no or incorrect error: %v", err) + return + } + if _, err := ww.ExportWatchingWallet(); err != ErrWalletIsWatchingOnly { + t.Errorf("Nonsensical func ExportWatchingWallet returned no or incorrect error: %v", err) + return + } + if _, err := ww.ImportPrivateKey(make([]byte, 32), true, createdAt); err != ErrWalletIsWatchingOnly { + t.Errorf("Nonsensical func ImportPrivateKey returned no or incorrect error: %v", err) + return + } +}