Implement exporting a watching-only wallet.

This change allows for the use of watching-only wallets.  Unlike
normal, "hot" wallets, watching-only wallets do not contain any
private keys, and can be used in situations where you want to keep one
wallet online to create new receiving addresses and watch for received
transactions, while keeping the hot wallet offline (possibly on an
air-gapped computer).

Two (websocket) extension RPC calls have been added:

First, exportwatchingwallet, which will export the current hot wallet
to a watching-only wallet, saving either to disk or returning the
base64-encoded wallet files to the caller.

Second, recoveraddresses, which is used to recover the next n
addresses from the address chain.  This is used to "sync" a watching
wallet with the hot wallet, or vice versa.
This commit is contained in:
Josh Rickmar 2014-01-21 14:45:28 -05:00
parent 8b65e651cd
commit bd89f076cd
3 changed files with 234 additions and 0 deletions

View file

@ -18,6 +18,7 @@ package main
import (
"bytes"
"encoding/base64"
"errors"
"fmt"
"github.com/conformal/btcutil"
@ -433,6 +434,58 @@ func (a *Account) ImportWIFPrivateKey(wif string, bs *wallet.BlockStamp) (string
return addrStr, nil
}
// ExportWatchingWallet returns a new account with a watching wallet
// exported by this a's wallet. Both wallets share the same tx and utxo
// stores, so locking one will lock the other as well. The returned account
// should be exported quickly, either to file or to an rpc caller, and then
// dropped from scope.
func (a *Account) ExportWatchingWallet() (*Account, error) {
a.mtx.RLock()
defer a.mtx.RUnlock()
ww, err := a.Wallet.ExportWatchingWallet()
if err != nil {
return nil, err
}
wa := *a
wa.Wallet = ww
return &wa, nil
}
// exportBase64 exports an account's serialized wallet, tx, and utxo
// stores as base64-encoded values in a map.
func (a *Account) exportBase64() (map[string]string, error) {
buf := &bytes.Buffer{}
m := make(map[string]string)
a.mtx.RLock()
defer a.mtx.RUnlock()
if _, err := a.Wallet.WriteTo(buf); err != nil {
return nil, err
}
m["wallet"] = base64.StdEncoding.EncodeToString(buf.Bytes())
buf.Reset()
a.TxStore.RLock()
defer a.TxStore.RUnlock()
if _, err := a.TxStore.s.WriteTo(buf); err != nil {
return nil, err
}
m["tx"] = base64.StdEncoding.EncodeToString(buf.Bytes())
buf.Reset()
a.UtxoStore.RLock()
defer a.UtxoStore.RUnlock()
if _, err := a.UtxoStore.s.WriteTo(buf); err != nil {
return nil, err
}
m["utxo"] = base64.StdEncoding.EncodeToString(buf.Bytes())
buf.Reset()
return m, nil
}
// Track requests btcd to send notifications of new transactions for
// each address stored in a wallet.
func (a *Account) Track() {
@ -557,6 +610,41 @@ func (a *Account) NewAddress() (btcutil.Address, error) {
return addr, nil
}
// RecoverAddresses recovers the next n chained addresses of a wallet.
func (a *Account) RecoverAddresses(n int) error {
a.mtx.Lock()
// Get info on the last chained address. The rescan starts at the
// earliest block height the last chained address might appear at.
last := a.Wallet.LastChainedAddress()
lastInfo, err := a.Wallet.AddressInfo(last)
if err != nil {
a.mtx.Unlock()
return err
}
addrs, err := a.Wallet.ExtendActiveAddresses(n, cfg.KeypoolSize)
a.mtx.Unlock()
if err != nil {
return err
}
// Run a goroutine to rescan blockchain for recovered addresses.
m := make(map[string]struct{})
for i := range addrs {
m[addrs[i].EncodeAddress()] = struct{}{}
}
go func(addrs map[string]struct{}) {
jsonErr := Rescan(CurrentRPCConn(), lastInfo.FirstBlock, addrs)
if jsonErr != nil {
log.Errorf("Rescanning for recovered addresses failed: %v",
jsonErr.Message)
}
}(m)
return nil
}
// ReqNewTxsForAddress sends a message to btcd to request tx updates
// for addr for each new block that is added to the blockchain.
func (a *Account) ReqNewTxsForAddress(addr btcutil.Address) {

View file

@ -87,10 +87,12 @@ var rpcHandlers = map[string]cmdHandler{
// Extensions exclusive to websocket connections.
var wsHandlers = map[string]cmdHandler{
"exportwatchingwallet": ExportWatchingWallet,
"getaddressbalance": GetAddressBalance,
"getunconfirmedbalance": GetUnconfirmedBalance,
"listaddresstransactions": ListAddressTransactions,
"listalltransactions": ListAllTransactions,
"recoveraddresses": RecoverAddresses,
"walletislocked": WalletIsLocked,
}
@ -224,6 +226,68 @@ func DumpWallet(icmd btcjson.Cmd) (interface{}, *btcjson.Error) {
}
}
// ExportWatchingWallet handles an exportwatchingwallet request by exporting
// the current account wallet as a watching wallet (with no private keys), and
// either writing the exported wallet to disk, or base64-encoding serialized
// account files and sending them back in the response.
func ExportWatchingWallet(icmd btcjson.Cmd) (interface{}, *btcjson.Error) {
// Type assert icmd to access parameters.
cmd, ok := icmd.(*btcws.ExportWatchingWalletCmd)
if !ok {
return nil, &btcjson.ErrInternal
}
a, err := accountstore.Account(cmd.Account)
switch err {
case nil:
break
case ErrAcctNotExist:
return nil, &btcjson.ErrWalletInvalidAccountName
default: // all other non-nil errors
e := btcjson.Error{
Code: btcjson.ErrWallet.Code,
Message: err.Error(),
}
return nil, &e
}
wa, err := a.ExportWatchingWallet()
if err != nil {
e := btcjson.Error{
Code: btcjson.ErrWallet.Code,
Message: err.Error(),
}
return nil, &e
}
if cmd.Download {
switch m, err := wa.exportBase64(); err {
case nil:
return m, nil
default:
e := btcjson.Error{
Code: btcjson.ErrWallet.Code,
Message: err.Error(),
}
return nil, &e
}
}
// Create export directory, write files there.
if err = wa.WriteExport("watchingwallet"); err != nil {
e := btcjson.Error{
Code: btcjson.ErrWallet.Code,
Message: err.Error(),
}
return nil, &e
}
return nil, nil
}
// GetAddressesByAccount handles a getaddressesbyaccount request by returning
// all addresses for an account, or an error if the requested account does
// not exist.
@ -1081,6 +1145,39 @@ func CreateEncryptedWallet(icmd btcjson.Cmd) (interface{}, *btcjson.Error) {
}
}
func RecoverAddresses(icmd btcjson.Cmd) (interface{}, *btcjson.Error) {
cmd, ok := icmd.(*btcws.RecoverAddressesCmd)
if !ok {
return nil, &btcjson.ErrInternal
}
a, err := accountstore.Account(cmd.Account)
switch err {
case nil:
break
case ErrAcctNotExist:
return nil, &btcjson.ErrWalletInvalidAccountName
default: // all other non-nil errors
e := btcjson.Error{
Code: btcjson.ErrWallet.Code,
Message: err.Error(),
}
return nil, &e
}
if err := a.RecoverAddresses(cmd.N); err != nil {
e := btcjson.Error{
Code: btcjson.ErrWallet.Code,
Message: err.Error(),
}
return nil, &e
}
return nil, nil
}
// WalletIsLocked handles the walletislocked extension request by
// returning the current lock state (false for unlocked, true for locked)
// of an account. An error is returned if the requested account does not

View file

@ -206,3 +206,52 @@ func (a *Account) writeDirtyToDisk() error {
return nil
}
// WriteExport writes an account to a special export directory named
// by dirName. Any previous files are overwritten.
func (a *Account) WriteExport(dirName string) error {
exportPath := filepath.Join(networkDir(cfg.Net()), dirName)
if err := checkCreateDir(exportPath); err != nil {
return err
}
aname := a.Name()
wfilepath := accountFilename("wallet.bin", aname, exportPath)
txfilepath := accountFilename("tx.bin", aname, exportPath)
utxofilepath := accountFilename("utxo.bin", aname, exportPath)
a.UtxoStore.RLock()
defer a.UtxoStore.RUnlock()
utxofile, err := os.Create(utxofilepath)
if err != nil {
return err
}
defer utxofile.Close()
if _, err := a.UtxoStore.s.WriteTo(utxofile); err != nil {
return err
}
a.TxStore.RLock()
defer a.TxStore.RUnlock()
txfile, err := os.Create(txfilepath)
if err != nil {
return err
}
defer txfile.Close()
if _, err := a.TxStore.s.WriteTo(txfile); err != nil {
return err
}
a.mtx.RLock()
defer a.mtx.RUnlock()
wfile, err := os.Create(wfilepath)
if err != nil {
return err
}
defer wfile.Close()
if _, err := a.Wallet.WriteTo(wfile); err != nil {
return err
}
return nil
}