From bd89f076cddc8d9af01e0bae1702275f6be49470 Mon Sep 17 00:00:00 2001 From: Josh Rickmar Date: Tue, 21 Jan 2014 14:45:28 -0500 Subject: [PATCH] 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. --- account.go | 88 ++++++++++++++++++++++++++++++++++++++++++++++++ cmdmgr.go | 97 +++++++++++++++++++++++++++++++++++++++++++++++++++++ disksync.go | 49 +++++++++++++++++++++++++++ 3 files changed, 234 insertions(+) diff --git a/account.go b/account.go index 05028f6..23fea34 100644 --- a/account.go +++ b/account.go @@ -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) { diff --git a/cmdmgr.go b/cmdmgr.go index cf8f9d8..3aba762 100644 --- a/cmdmgr.go +++ b/cmdmgr.go @@ -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 diff --git a/disksync.go b/disksync.go index 26e9d54..573e4fd 100644 --- a/disksync.go +++ b/disksync.go @@ -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 +}