Add function+tests for exporting a watching wallet.
This change introduces a new function to export a wallet in memory to a watching wallet. Watching wallets allow to watch for balance changes and transactions to wallet addresses while only storing the public parts of a wallet (no private keys). New addresses created by the watching wallet will use pubkey address chaining and will allow to receive funds to an indefinite number of new addresses, and create the private keys for said addresses from the non-watching wallet later. The actual exporting of a watching wallet to a file (triggered by an RPC request) is not yet implemented. While here, fix an issue found by new test code for the chained address code which incorrectly set the starting index of addresses in the chain needing private keys to be created.
This commit is contained in:
parent
8952fc5acf
commit
effd810e54
3 changed files with 310 additions and 38 deletions
|
@ -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
|
||||
|
|
182
wallet/wallet.go
182
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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue