From b9fd527d33ae09d37861977fb23babb46bbf821e Mon Sep 17 00:00:00 2001 From: Josh Rickmar Date: Tue, 8 Jul 2014 22:17:38 -0500 Subject: [PATCH] Remove account support, fix races on btcd connect. This commit is the result of several big changes being made to the wallet. In particular, the "handshake" (initial sync to the chain server) was quite racy and required proper synchronization. To make fixing this race easier, several other changes were made to the internal wallet data structures and much of the RPC server ended up being rewritten. First, all account support has been removed. The previous Account struct has been replaced with a Wallet structure, which includes a keystore for saving keys, and a txstore for storing relevant transactions. This decision has been made since it is the opinion of myself and other developers that bitcoind accounts are fundamentally broken (as accounts implemented by bitcoind support both arbitrary address groupings as well as moving balances between accounts -- these are fundamentally incompatible features), and since a BIP0032 keystore is soon planned to be implemented (at which point, "accounts" can return as HD extended keys). With the keystore handling the grouping of related keys, there is no reason have many different Account structs, and the AccountManager has been removed as well. All RPC handlers that take an account option will only work with "" (the default account) or "*" if the RPC allows specifying all accounts. Second, much of the RPC server has been cleaned up. The global variables for the RPC server and chain server client have been moved to part of the rpcServer struct, and the handlers for each RPC method that are looked up change depending on which components have been set. Passthrough requests are also no longer handled specially, but when the chain server is set, a handler to perform the passthrough will be returned if the method is not otherwise a wallet RPC. The notification system for websocket clients has also been rewritten so wallet components can send notifications through channels, rather than requiring direct access to the RPC server itself, or worse still, sending directly to a websocket client's send channel. In the future, this will enable proper registration of notifications, rather than unsolicited broadcasts to every connected websocket client (see issue #84). Finally, and the main reason why much of this cleanup was necessary, the races during intial sync with the chain server have been fixed. Previously, when the 'Handshake' was run, a rescan would occur which would perform modifications to Account data structures as notifications were received. Synchronization was provided with a single binary semaphore which serialized all access to wallet and account data. However, the Handshake itself was not able to run with this lock (or else notifications would block), and many data races would occur as both notifications were being handled. If GOMAXPROCS was ever increased beyond 1, btcwallet would always immediately crash due to invalid addresses caused by the data races on startup. To fix this, the single lock for all wallet access has been replaced with mutexes for both the keystore and txstore. Handling of btcd notifications and client requests may now occur simultaneously. GOMAXPROCS has also been set to the number of logical CPUs at the beginning of main, since with the data races fixed, there's no reason to prevent the extra parallelism gained by increasing it. Closes #78. Closes #101. Closes #110. --- account.go | 778 ----- acctmgr.go | 1130 -------- btcwallet.go | 168 ++ chain/chain.go | 306 ++ chain/log.go | 68 + chainntfns.go | 182 ++ cmd.go | 161 -- config.go | 4 +- createtx.go | 60 +- createtx_test.go | 2 +- disksync.go | 396 --- keystore/keystore.go | 731 +++-- keystore/keystore_test.go | 178 +- log.go | 6 + rename_plan9.go => rename/rename_plan9.go | 6 +- rename_unix.go => rename/rename_unix.go | 6 +- rename_windows.go => rename/rename_windows.go | 6 +- rescan.go | 456 +-- rpcclient.go | 565 ---- rpcserver.go | 2493 ++++++++--------- txstore/json.go | 48 +- txstore/notifications.go | 145 + txstore/serialization.go | 88 +- txstore/tx.go | 210 +- txstore/tx_test.go | 14 +- updates.go | 166 -- wallet.go | 1367 +++++++++ 27 files changed, 4427 insertions(+), 5313 deletions(-) delete mode 100644 account.go delete mode 100644 acctmgr.go create mode 100644 btcwallet.go create mode 100644 chain/chain.go create mode 100644 chain/log.go create mode 100644 chainntfns.go delete mode 100644 cmd.go rename rename_plan9.go => rename/rename_plan9.go (93%) rename rename_unix.go => rename/rename_unix.go (87%) rename rename_windows.go => rename/rename_windows.go (94%) delete mode 100644 rpcclient.go create mode 100644 txstore/notifications.go delete mode 100644 updates.go create mode 100644 wallet.go diff --git a/account.go b/account.go deleted file mode 100644 index 5211154..0000000 --- a/account.go +++ /dev/null @@ -1,778 +0,0 @@ -/* - * Copyright (c) 2013, 2014 Conformal Systems LLC - * - * Permission to use, copy, modify, and distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - */ - -package main - -import ( - "bytes" - "encoding/base64" - "fmt" - "path/filepath" - - "github.com/conformal/btcjson" - "github.com/conformal/btcutil" - "github.com/conformal/btcwallet/keystore" - "github.com/conformal/btcwallet/txstore" - "github.com/conformal/btcwire" -) - -// Account is a structure containing all the components for a -// complete wallet. It contains the Armory-style wallet (to store -// addresses and keys), and tx and utxo stores, and a mutex to prevent -// incorrect multiple access. -type Account struct { - name string - KeyStore *keystore.Store - TxStore *txstore.Store - lockedOutpoints map[btcwire.OutPoint]struct{} - FeeIncrement btcutil.Amount -} - -func newAccount(name string, keys *keystore.Store, txs *txstore.Store) *Account { - return &Account{ - name: name, - KeyStore: keys, - TxStore: txs, - lockedOutpoints: map[btcwire.OutPoint]struct{}{}, - FeeIncrement: defaultFeeIncrement, - } -} - -// Lock locks the underlying wallet for an account. -func (a *Account) Lock() error { - switch err := a.KeyStore.Lock(); err { - case nil: - server.NotifyWalletLockStateChange(a.KeyStore.Name(), true) - return nil - - case keystore.ErrLocked: - // Do not pass wallet already locked errors to the caller. - return nil - - default: - return err - } -} - -// Unlock unlocks the underlying wallet for an account. -func (a *Account) Unlock(passphrase []byte) error { - if err := a.KeyStore.Unlock(passphrase); err != nil { - return err - } - - server.NotifyWalletLockStateChange(a.KeyStore.Name(), false) - return nil -} - -// AddressUsed returns whether there are any recorded transactions spending to -// a given address. Assumming correct TxStore usage, this will return true iff -// there are any transactions with outputs to this address in the blockchain or -// the btcd mempool. -func (a *Account) AddressUsed(addr btcutil.Address) bool { - // This not only can be optimized by recording this data as it is - // read when opening an account, and keeping it up to date each time a - // new received tx arrives, but it probably should in case an address is - // used in a tx (made public) but the tx is eventually removed from the - // store (consider a chain reorg). - - pkHash := addr.ScriptAddress() - - for _, r := range a.TxStore.Records() { - credits := r.Credits() - for _, c := range credits { - // Errors don't matter here. If addrs is nil, the - // range below does nothing. - _, addrs, _, _ := c.Addresses(activeNet.Params) - for _, a := range addrs { - if bytes.Equal(a.ScriptAddress(), pkHash) { - return true - } - } - } - } - return false -} - -// CalculateBalance sums the amounts of all unspent transaction -// outputs to addresses of a wallet and returns the balance as a -// float64. -// -// If confirmations is 0, all UTXOs, even those not present in a -// block (height -1), will be used to get the balance. Otherwise, -// a UTXO must be in a block. If confirmations is 1 or greater, -// the balance will be calculated based on how many how many blocks -// include a UTXO. -func (a *Account) CalculateBalance(confirms int) (btcutil.Amount, error) { - rpcc, err := accessClient() - if err != nil { - return 0, err - } - bs, err := rpcc.BlockStamp() - if err != nil { - return 0, err - } - - return a.TxStore.Balance(confirms, bs.Height) -} - -// CalculateAddressBalance sums the amounts of all unspent transaction -// outputs to a single address's pubkey hash and returns the balance -// as a float64. -// -// If confirmations is 0, all UTXOs, even those not present in a -// block (height -1), will be used to get the balance. Otherwise, -// a UTXO must be in a block. If confirmations is 1 or greater, -// the balance will be calculated based on how many how many blocks -// include a UTXO. -func (a *Account) CalculateAddressBalance(addr btcutil.Address, confirms int) (btcutil.Amount, error) { - rpcc, err := accessClient() - if err != nil { - return 0, err - } - bs, err := rpcc.BlockStamp() - if err != nil { - return 0, err - } - - var bal btcutil.Amount - unspent, err := a.TxStore.UnspentOutputs() - if err != nil { - return 0, err - } - for _, credit := range unspent { - if credit.Confirmed(confirms, bs.Height) { - // We only care about the case where len(addrs) == 1, and err - // will never be non-nil in that case - _, addrs, _, _ := credit.Addresses(activeNet.Params) - if len(addrs) != 1 { - continue - } - if addrs[0].EncodeAddress() == addr.EncodeAddress() { - bal += credit.Amount() - } - } - } - return bal, nil -} - -// CurrentAddress gets the most recently requested Bitcoin payment address -// from an account. If the address has already been used (there is at least -// one transaction spending to it in the blockchain or btcd mempool), the next -// chained address is returned. -func (a *Account) CurrentAddress() (btcutil.Address, error) { - addr := a.KeyStore.LastChainedAddress() - - // Get next chained address if the last one has already been used. - if a.AddressUsed(addr) { - return a.NewAddress() - } - - return addr, nil -} - -// ListSinceBlock returns a slice of objects with details about transactions -// since the given block. If the block is -1 then all transactions are included. -// This is intended to be used for listsinceblock RPC replies. -func (a *Account) ListSinceBlock(since, curBlockHeight int32, - minconf int) ([]btcjson.ListTransactionsResult, error) { - - txList := []btcjson.ListTransactionsResult{} - for _, txRecord := range a.TxStore.Records() { - // Transaction records must only be considered if they occur - // after the block height since. - if since != -1 && txRecord.BlockHeight <= since { - continue - } - - // Transactions that have not met minconf confirmations are to - // be ignored. - if !txRecord.Confirmed(minconf, curBlockHeight) { - continue - } - - jsonResults, err := txRecord.ToJSON(a.name, curBlockHeight, - a.KeyStore.Net()) - if err != nil { - return nil, err - } - txList = append(txList, jsonResults...) - } - - return txList, nil -} - -// ListTransactions returns a slice of objects with details about a recorded -// transaction. This is intended to be used for listtransactions RPC -// replies. -func (a *Account) ListTransactions(from, count int) ([]btcjson.ListTransactionsResult, error) { - txList := []btcjson.ListTransactionsResult{} - - // Get current block. The block height used for calculating - // the number of tx confirmations. - rpcc, err := accessClient() - if err != nil { - return txList, err - } - bs, err := rpcc.BlockStamp() - if err != nil { - return txList, err - } - - records := a.TxStore.Records() - lastLookupIdx := len(records) - count - // Search in reverse order: lookup most recently-added first. - for i := len(records) - 1; i >= from && i >= lastLookupIdx; i-- { - jsonResults, err := records[i].ToJSON(a.name, bs.Height, - a.KeyStore.Net()) - if err != nil { - return nil, err - } - txList = append(txList, jsonResults...) - } - - return txList, nil -} - -// ListAddressTransactions returns a slice of objects with details about -// recorded transactions to or from any address belonging to a set. This is -// intended to be used for listaddresstransactions RPC replies. -func (a *Account) ListAddressTransactions(pkHashes map[string]struct{}) ( - []btcjson.ListTransactionsResult, error) { - - txList := []btcjson.ListTransactionsResult{} - - // Get current block. The block height used for calculating - // the number of tx confirmations. - rpcc, err := accessClient() - if err != nil { - return txList, err - } - bs, err := rpcc.BlockStamp() - if err != nil { - return txList, err - } - - for _, r := range a.TxStore.Records() { - for _, c := range r.Credits() { - // We only care about the case where len(addrs) == 1, - // and err will never be non-nil in that case. - _, addrs, _, _ := c.Addresses(activeNet.Params) - if len(addrs) != 1 { - continue - } - apkh, ok := addrs[0].(*btcutil.AddressPubKeyHash) - if !ok { - continue - } - - if _, ok := pkHashes[string(apkh.ScriptAddress())]; !ok { - continue - } - jsonResult, err := c.ToJSON(a.name, bs.Height, - a.KeyStore.Net()) - if err != nil { - return nil, err - } - txList = append(txList, jsonResult) - } - } - - return txList, nil -} - -// ListAllTransactions returns a slice of objects with details about a recorded -// transaction. This is intended to be used for listalltransactions RPC -// replies. -func (a *Account) ListAllTransactions() ([]btcjson.ListTransactionsResult, error) { - txList := []btcjson.ListTransactionsResult{} - - // Get current block. The block height used for calculating - // the number of tx confirmations. - rpcc, err := accessClient() - if err != nil { - return txList, err - } - bs, err := rpcc.BlockStamp() - if err != nil { - return txList, err - } - - // Search in reverse order: lookup most recently-added first. - records := a.TxStore.Records() - for i := len(records) - 1; i >= 0; i-- { - jsonResults, err := records[i].ToJSON(a.name, bs.Height, - a.KeyStore.Net()) - if err != nil { - return nil, err - } - txList = append(txList, jsonResults...) - } - - return txList, nil -} - -// DumpPrivKeys returns the WIF-encoded private keys for all addresses with -// private keys in a wallet. -func (a *Account) DumpPrivKeys() ([]string, error) { - // Iterate over each active address, appending the private - // key to privkeys. - privkeys := []string{} - for _, info := range a.KeyStore.ActiveAddresses() { - // Only those addresses with keys needed. - pka, ok := info.(keystore.PubKeyAddress) - if !ok { - continue - } - wif, err := pka.ExportPrivKey() - if err != nil { - // It would be nice to zero out the array here. However, - // since strings in go are immutable, and we have no - // control over the caller I don't think we can. :( - return nil, err - } - privkeys = append(privkeys, wif.String()) - } - - return privkeys, nil -} - -// DumpWIFPrivateKey returns the WIF encoded private key for a -// single wallet address. -func (a *Account) DumpWIFPrivateKey(addr btcutil.Address) (string, error) { - // Get private key from wallet if it exists. - address, err := a.KeyStore.Address(addr) - if err != nil { - return "", err - } - - pka, ok := address.(keystore.PubKeyAddress) - if !ok { - return "", fmt.Errorf("address %s is not a key type", addr) - } - - wif, err := pka.ExportPrivKey() - if err != nil { - return "", err - } - return wif.String(), nil -} - -// ImportPrivateKey imports a private key to the account's wallet and -// writes the new wallet to disk. -func (a *Account) ImportPrivateKey(wif *btcutil.WIF, bs *keystore.BlockStamp, - rescan bool) (string, error) { - - // Attempt to import private key into wallet. - addr, err := a.KeyStore.ImportPrivateKey(wif, bs) - if err != nil { - return "", err - } - - // Immediately write wallet to disk. - AcctMgr.ds.ScheduleWalletWrite(a) - if err := AcctMgr.ds.FlushAccount(a); err != nil { - return "", fmt.Errorf("cannot write account: %v", err) - } - - addrStr := addr.EncodeAddress() - - // Rescan blockchain for transactions with txout scripts paying to the - // imported address. - if rescan { - addrs := []btcutil.Address{addr} - job := &RescanJob{ - Addresses: map[*Account][]btcutil.Address{a: addrs}, - OutPoints: nil, - StartHeight: 0, - } - - // Submit rescan job and log when the import has completed. - // Do not block on finishing the rescan. - doneChan := AcctMgr.rm.SubmitJob(job) - go func() { - <-doneChan - log.Infof("Finished import for address %s", addrStr) - }() - } - - // Associate the imported address with this account. - AcctMgr.MarkAddressForAccount(addr, a) - - log.Infof("Imported payment address %s", addrStr) - - // Return the payment address string of the imported private key. - return addrStr, nil -} - -// ExportToDirectory writes an account to a special export directory. Any -// previous files are overwritten. -func (a *Account) ExportToDirectory(dirBaseName string) error { - dir := filepath.Join(networkDir(activeNet.Params), dirBaseName) - if err := checkCreateDir(dir); err != nil { - return err - } - - return AcctMgr.ds.ExportAccount(a, dir) -} - -// 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) { - ww, err := a.KeyStore.ExportWatchingWallet() - if err != nil { - return nil, err - } - - wa := *a - wa.KeyStore = 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) - - _, err := a.KeyStore.WriteTo(&buf) - if err != nil { - return nil, err - } - m["wallet"] = base64.StdEncoding.EncodeToString(buf.Bytes()) - buf.Reset() - - if _, err = a.TxStore.WriteTo(&buf); err != nil { - return nil, err - } - m["tx"] = base64.StdEncoding.EncodeToString(buf.Bytes()) - buf.Reset() - - return m, nil -} - -// LockedOutpoint returns whether an outpoint has been marked as locked and -// should not be used as an input for created transactions. -func (a *Account) LockedOutpoint(op btcwire.OutPoint) bool { - _, locked := a.lockedOutpoints[op] - return locked -} - -// LockOutpoint marks an outpoint as locked, that is, it should not be used as -// an input for newly created transactions. -func (a *Account) LockOutpoint(op btcwire.OutPoint) { - a.lockedOutpoints[op] = struct{}{} -} - -// UnlockOutpoint marks an outpoint as unlocked, that is, it may be used as an -// input for newly created transactions. -func (a *Account) UnlockOutpoint(op btcwire.OutPoint) { - delete(a.lockedOutpoints, op) -} - -// ResetLockedOutpoints resets the set of locked outpoints so all may be used -// as inputs for new transactions. -func (a *Account) ResetLockedOutpoints() { - a.lockedOutpoints = map[btcwire.OutPoint]struct{}{} -} - -// LockedOutpoints returns a slice of currently locked outpoints. This is -// intended to be used by marshaling the result as a JSON array for -// listlockunspent RPC results. -func (a *Account) LockedOutpoints() []btcjson.TransactionInput { - locked := make([]btcjson.TransactionInput, len(a.lockedOutpoints)) - i := 0 - for op := range a.lockedOutpoints { - locked[i] = btcjson.TransactionInput{ - Txid: op.Hash.String(), - Vout: op.Index, - } - i++ - } - return locked -} - -// Track requests btcd to send notifications of new transactions for -// each address stored in a wallet. -func (a *Account) Track() { - rpcc, err := accessClient() - if err != nil { - log.Errorf("No chain server client to track addresses.") - return - } - - // Request notifications for transactions sending to all wallet - // addresses. - // - // TODO: return as slice? (doesn't have to be ordered, or - // SortedActiveAddresses would be fine.) - addrMap := a.KeyStore.ActiveAddresses() - addrs := make([]btcutil.Address, 0, len(addrMap)) - for addr := range addrMap { - addrs = append(addrs, addr) - } - - if err := rpcc.NotifyReceived(addrs); err != nil { - log.Error("Unable to request transaction updates for address.") - } - - unspent, err := a.TxStore.UnspentOutputs() - if err != nil { - log.Errorf("Unable to access unspent outputs: %v", err) - return - } - ReqSpentUtxoNtfns(unspent) -} - -// RescanActiveJob creates a RescanJob for all active addresses in the -// account. This is needed for catching btcwallet up to a long-running -// btcd process, as otherwise it would have missed notifications as -// blocks are attached to the main chain. -func (a *Account) RescanActiveJob() (*RescanJob, error) { - // Determine the block necesary to start the rescan for all active - // addresses. - height := a.KeyStore.SyncHeight() - - actives := a.KeyStore.SortedActiveAddresses() - addrs := make([]btcutil.Address, 0, len(actives)) - for i := range actives { - addrs = append(addrs, actives[i].Address()) - } - - unspents, err := a.TxStore.UnspentOutputs() - if err != nil { - return nil, err - } - outpoints := make([]*btcwire.OutPoint, 0, len(unspents)) - for _, c := range unspents { - outpoints = append(outpoints, c.OutPoint()) - } - - job := &RescanJob{ - Addresses: map[*Account][]btcutil.Address{a: addrs}, - OutPoints: outpoints, - StartHeight: height, - } - return job, nil -} - -// ResendUnminedTxs iterates through all transactions that spend from wallet -// credits that are not known to have been mined into a block, and attempts -// to send each to the chain server for relay. -func (a *Account) ResendUnminedTxs() { - rpcc, err := accessClient() - if err != nil { - log.Errorf("No chain server client to resend txs.") - return - } - - txs := a.TxStore.UnminedDebitTxs() - for _, tx := range txs { - _, err := rpcc.SendRawTransaction(tx.MsgTx(), false) - if err != nil { - // TODO(jrick): Check error for if this tx is a double spend, - // remove it if so. - log.Warnf("Could not resend transaction %v: %v", - tx.Sha(), err) - continue - } - log.Debugf("Resent unmined transaction %v", tx.Sha()) - } -} - -// SortedActivePaymentAddresses returns a slice of all active payment -// addresses in an account. -func (a *Account) SortedActivePaymentAddresses() []string { - infos := a.KeyStore.SortedActiveAddresses() - - addrs := make([]string, len(infos)) - for i, info := range infos { - addrs[i] = info.Address().EncodeAddress() - } - - return addrs -} - -// ActivePaymentAddresses returns a set of all active pubkey hashes -// in an account. -func (a *Account) ActivePaymentAddresses() map[string]struct{} { - infos := a.KeyStore.ActiveAddresses() - - addrs := make(map[string]struct{}, len(infos)) - for _, info := range infos { - addrs[info.Address().EncodeAddress()] = struct{}{} - } - - return addrs -} - -// NewAddress returns a new payment address for an account. -func (a *Account) NewAddress() (btcutil.Address, error) { - // Get current block's height and hash. - rpcc, err := accessClient() - if err != nil { - return nil, err - } - bs, err := rpcc.BlockStamp() - if err != nil { - return nil, err - } - - // Get next address from wallet. - addr, err := a.KeyStore.NextChainedAddress(&bs, cfg.KeypoolSize) - if err != nil { - return nil, err - } - - // Immediately write updated wallet to disk. - AcctMgr.ds.ScheduleWalletWrite(a) - if err := AcctMgr.ds.FlushAccount(a); err != nil { - return nil, fmt.Errorf("account write failed: %v", err) - } - - // Mark this new address as belonging to this account. - AcctMgr.MarkAddressForAccount(addr, a) - - // Request updates from btcd for new transactions sent to this address. - if err := rpcc.NotifyReceived([]btcutil.Address{addr}); err != nil { - return nil, err - } - - return addr, nil -} - -// NewChangeAddress returns a new change address for an account. -func (a *Account) NewChangeAddress() (btcutil.Address, error) { - // Get current block's height and hash. - rpcc, err := accessClient() - if err != nil { - return nil, err - } - bs, err := rpcc.BlockStamp() - if err != nil { - return nil, err - } - - // Get next chained change address from wallet. - addr, err := a.KeyStore.ChangeAddress(&bs, cfg.KeypoolSize) - if err != nil { - return nil, err - } - - // Immediately write updated wallet to disk. - AcctMgr.ds.ScheduleWalletWrite(a) - if err := AcctMgr.ds.FlushAccount(a); err != nil { - return nil, fmt.Errorf("account write failed: %v", err) - } - - // Mark this new address as belonging to this account. - AcctMgr.MarkAddressForAccount(addr, a) - - // Request updates from btcd for new transactions sent to this address. - if err := rpcc.NotifyReceived([]btcutil.Address{addr}); err != nil { - return nil, err - } - - return addr, nil -} - -// RecoverAddresses recovers the next n chained addresses of a wallet. -func (a *Account) RecoverAddresses(n int) error { - // Get info on the last chained address. The rescan starts at the - // earliest block height the last chained address might appear at. - last := a.KeyStore.LastChainedAddress() - lastInfo, err := a.KeyStore.Address(last) - if err != nil { - return err - } - - addrs, err := a.KeyStore.ExtendActiveAddresses(n, cfg.KeypoolSize) - if err != nil { - return err - } - - // Run a goroutine to rescan blockchain for recovered addresses. - go func() { - rpcc, err := accessClient() - if err != nil { - log.Errorf("Cannot access chain server client to " + - "rescan recovered addresses.") - return - } - err = rpcc.Rescan(lastInfo.FirstBlock(), addrs, nil) - if err != nil { - log.Errorf("Rescanning for recovered addresses "+ - "failed: %v", err) - } - }() - - return nil -} - -// ReqSpentUtxoNtfns sends a message to btcd to request updates for when -// a stored UTXO has been spent. -func ReqSpentUtxoNtfns(credits []txstore.Credit) { - ops := make([]*btcwire.OutPoint, 0, len(credits)) - for _, c := range credits { - op := c.OutPoint() - log.Debugf("Requesting spent UTXO notifications for Outpoint "+ - "hash %s index %d", op.Hash, op.Index) - ops = append(ops, op) - } - - rpcc, err := accessClient() - if err != nil { - log.Errorf("Cannot access chain server client to " + - "request spent output notifications.") - return - } - if err := rpcc.NotifySpent(ops); err != nil { - log.Errorf("Cannot request notifications for spent outputs: %v", - err) - } -} - -// TotalReceived iterates through an account's transaction history, returning the -// total amount of bitcoins received for any account address. Amounts received -// through multisig transactions are ignored. -func (a *Account) TotalReceived(confirms int) (btcutil.Amount, error) { - rpcc, err := accessClient() - if err != nil { - return 0, err - } - bs, err := rpcc.BlockStamp() - if err != nil { - return 0, err - } - - var amount btcutil.Amount - for _, r := range a.TxStore.Records() { - for _, c := range r.Credits() { - // Ignore change. - if c.Change() { - continue - } - - // Tally if the appropiate number of block confirmations have passed. - if c.Confirmed(confirms, bs.Height) { - amount += c.Amount() - } - } - } - return amount, nil -} diff --git a/acctmgr.go b/acctmgr.go deleted file mode 100644 index eb4227d..0000000 --- a/acctmgr.go +++ /dev/null @@ -1,1130 +0,0 @@ -/* - * Copyright (c) 2013, 2014 Conformal Systems LLC - * - * Permission to use, copy, modify, and distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - */ - -package main - -import ( - "encoding/hex" - "errors" - "fmt" - "io/ioutil" - "os" - "path/filepath" - "strings" - "sync" - "time" - - "github.com/conformal/btcchain" - "github.com/conformal/btcjson" - "github.com/conformal/btcutil" - "github.com/conformal/btcwallet/keystore" - "github.com/conformal/btcwallet/txstore" - "github.com/conformal/btcwire" -) - -// Errors relating to accounts. -var ( - ErrAccountExists = errors.New("account already exists") - ErrWalletExists = errors.New("wallet already exists") - ErrNotFound = errors.New("not found") - ErrNoAccounts = errors.New("no accounts") -) - -// AcctMgr is the global account manager for all opened accounts. -var AcctMgr = NewAccountManager() - -type ( - openAccountsCmd struct{} - - accessAccountRequest struct { - name string - resp chan *Account - } - - accessAllRequest struct { - resp chan []*Account - } - - accessAccountByAddressRequest struct { - address string - resp chan *Account - } - - markAddressForAccountCmd struct { - address string - account *Account - } - - addAccountCmd struct { - a *Account - } - - removeAccountCmd struct { - a *Account - } - - quitCmd struct{} -) - -type unlockRequest struct { - passphrase []byte - timeout time.Duration // Zero value prevents the timeout. - err chan error -} - -// AccountManager manages a collection of accounts. -type AccountManager struct { - // The accounts accessed through the account manager are not safe for - // concurrent access. The account manager therefore contains a - // binary semaphore channel to prevent incorrect access. - bsem chan struct{} - cmdChan chan interface{} - rescanMsgs chan RescanMsg - unlockRequests chan unlockRequest - lockRequests chan struct{} - unlockedState chan bool - - ds *DiskSyncer - rm *RescanManager - - wg sync.WaitGroup - quit chan struct{} -} - -// NewAccountManager returns a new AccountManager. -func NewAccountManager() *AccountManager { - am := &AccountManager{ - bsem: make(chan struct{}, 1), - cmdChan: make(chan interface{}), - rescanMsgs: make(chan RescanMsg, 1), - unlockRequests: make(chan unlockRequest), - lockRequests: make(chan struct{}), - unlockedState: make(chan bool), - - quit: make(chan struct{}), - } - am.ds = NewDiskSyncer(am) - am.rm = NewRescanManager(am.rescanMsgs) - return am -} - -// Start starts the goroutines required to run the AccountManager. -func (am *AccountManager) Start() { - // Ready the semaphore - can't grab unless the manager has started. - am.bsem <- struct{}{} - - am.wg.Add(3) - go am.accountHandler() - go am.keystoreLocker() - go am.rescanListener() - - go am.ds.Start() - go am.rm.Start() -} - -// Stop shuts down the account manager by stoping all signaling all goroutines -// started by Start to close. -func (am *AccountManager) Stop() { - am.rm.Stop() - am.ds.Stop() - close(am.quit) -} - -// WaitForShutdown blocks until all goroutines started by Start and stopped -// with Stop have finished. -func (am *AccountManager) WaitForShutdown() { - am.rm.WaitForShutdown() - am.ds.WaitForShutdown() - am.wg.Wait() -} - -// accountData is a helper structure to let us centralise logic for adding -// and removing accounts. -type accountData struct { - // maps name to account struct. We could keep a list here for iteration - // but iteration over the small amounts we have is likely not worth - // the extra complexity. - nameToAccount map[string]*Account - addressToAccount map[string]*Account -} - -func newAccountData() *accountData { - return &accountData{ - nameToAccount: make(map[string]*Account), - addressToAccount: make(map[string]*Account), - } -} - -func (ad *accountData) addAccount(a *Account) { - if _, ok := ad.nameToAccount[a.name]; ok { - return - } - ad.nameToAccount[a.name] = a - for addr := range a.ActivePaymentAddresses() { - ad.addressToAccount[addr] = a - } -} - -func (ad *accountData) removeAccount(a *Account) { - a, ok := ad.nameToAccount[a.name] - if !ok { - return - } - - delete(ad.nameToAccount, a.name) - for addr := range a.ActivePaymentAddresses() { - delete(ad.addressToAccount, addr) - } -} - -// walletOpenError is a special error type so problems opening wallet -// files can be differentiated (by a type assertion) from other errors. -type walletOpenError struct { - Err string -} - -// Error satisifies the builtin error interface. -func (e *walletOpenError) Error() string { - return e.Err -} - -var ( - // errNoWallet describes an error where a wallet does not exist and - // must be created first. - errNoWallet = &walletOpenError{ - Err: "wallet file does not exist", - } -) - -// openSavedAccount opens a named account from disk. If the wallet does not -// exist, errNoWallet is returned as an error. -func openSavedAccount(name string, cfg *config) (*Account, error) { - netdir := networkDir(activeNet.Params) - if err := checkCreateDir(netdir); err != nil { - return nil, &walletOpenError{ - Err: err.Error(), - } - } - - keys := new(keystore.Store) - txs := txstore.New() - a := newAccount(name, keys, txs) - - walletPath := accountFilename("wallet.bin", name, netdir) - txstorePath := accountFilename("tx.bin", name, netdir) - - // Read wallet file. - walletFi, err := os.Open(walletPath) - if err != nil { - if os.IsNotExist(err) { - // Must create and save wallet first. - return nil, errNoWallet - } - msg := fmt.Sprintf("cannot open wallet file: %s", err) - return nil, &walletOpenError{msg} - } - if _, err = keys.ReadFrom(walletFi); err != nil { - if err := walletFi.Close(); err != nil { - log.Warnf("Cannot close wallet file: %v", err) - } - msg := fmt.Sprintf("Cannot read wallet: %s", err) - return nil, &walletOpenError{msg} - } - - // Read txstore file. If this fails, write a new empty transaction - // store to disk, mark the wallet as unsynced, and write the unsynced - // wallet to disk. - // - // This file is opened read/write so it may be truncated if a new empty - // transaction store must be written. - txstoreFi, err := os.OpenFile(txstorePath, os.O_RDWR, 0) - if err != nil { - if err := walletFi.Close(); err != nil { - log.Warnf("Cannot close wallet file: %v", err) - } - if err := writeUnsyncedWallet(a, walletPath); err != nil { - return nil, err - } - - // Create and write empty txstore, if it doesn't exist. - if !fileExists(txstorePath) { - log.Warn("Transaction store file missing") - if txstoreFi, err = os.Create(txstorePath); err != nil { - return nil, fmt.Errorf("cannot create new "+ - "txstore file: %v", err) - } - defer func() { - if err := txstoreFi.Close(); err != nil { - log.Warnf("Cannot close transaction "+ - "store file: %v", err) - } - }() - } else { - return nil, fmt.Errorf("transaction store file "+ - "exists but cannot be opened: %v", err) - } - - if _, err := txs.WriteTo(txstoreFi); err != nil { - log.Warn(err) - } - return a, nil - } - if _, err = txs.ReadFrom(txstoreFi); err != nil { - if err := walletFi.Close(); err != nil { - log.Warnf("Cannot close wallet file: %v", err) - } - if err := writeUnsyncedWallet(a, walletPath); err != nil { - return nil, err - } - - defer func() { - if err := txstoreFi.Close(); err != nil { - log.Warnf("Cannot close transaction store "+ - "file: %v", err) - } - }() - log.Warnf("Cannot read transaction store: %s", err) - if _, err := txstoreFi.Seek(0, os.SEEK_SET); err != nil { - return nil, err - } - if err := txstoreFi.Truncate(0); err != nil { - return nil, err - } - if _, err := txs.WriteTo(txstoreFi); err != nil { - log.Warn("Cannot write new transaction store: %v", err) - } - log.Infof("Wrote empty transaction store file") - return a, nil - } - - if err := walletFi.Close(); err != nil { - log.Warnf("Cannot close wallet file: %v", err) - } - if err := txstoreFi.Close(); err != nil { - log.Warnf("Cannot close transaction store file: %v", err) - } - return a, nil -} - -// writeUnsyncedWallet sets the wallet unsynced (to handle the case -// where the transaction store was unreadable) and atomically writes -// the new wallet file back to disk. The current wallet file on disk -// should be already closed, or this will error on Windows for ovewriting -// an open file. -func writeUnsyncedWallet(a *Account, path string) error { - // Mark wallet as unsynced and write back to disk. Later calls - // to SyncHeight will use the wallet creation height, or possibly - // an earlier height for imported keys. - netdir, _ := filepath.Split(path) - a.KeyStore.SetSyncedWith(nil) - tmpwallet, err := ioutil.TempFile(netdir, "wallet.bin") - if err != nil { - return fmt.Errorf("cannot create temporary wallet: %v", err) - } - if _, err := a.KeyStore.WriteTo(tmpwallet); err != nil { - return fmt.Errorf("cannot write back unsynced wallet: %v", err) - } - tmpwalletpath := tmpwallet.Name() - if err := tmpwallet.Close(); err != nil { - return fmt.Errorf("cannot close temporary wallet file: %v", err) - } - if err := Rename(tmpwalletpath, path); err != nil { - return fmt.Errorf("cannot move temporary wallet file: %v", err) - } - return nil -} - -// openAccounts attempts to open all saved accounts. -func openAccounts() *accountData { - ad := newAccountData() - - // If the network (account) directory is missing, but the temporary - // directory exists, move it. This is unlikely to happen, but possible, - // if writing out every account file at once to a tmp directory (as is - // done for changing a wallet passphrase) and btcwallet closes after - // removing the network directory but before renaming the temporary - // directory. - netDir := networkDir(activeNet.Params) - tmpNetDir := tmpNetworkDir(activeNet.Params) - if !fileExists(netDir) && fileExists(tmpNetDir) { - if err := Rename(tmpNetDir, netDir); err != nil { - log.Errorf("Cannot move temporary network dir: %v", err) - return ad - } - } - - // The default account must exist, or btcwallet acts as if no - // wallets/accounts have been created yet. - a, err := openSavedAccount("", cfg) - if err != nil { - log.Errorf("Cannot open default account: %v", err) - return ad - } - - ad.addAccount(a) - - // Read all filenames in the account directory, and look for any - // filenames matching '*-wallet.bin'. These are wallets for - // additional saved accounts. - accountDir, err := os.Open(netDir) - if err != nil { - // Can't continue. - log.Errorf("Unable to open account directory: %v", err) - return ad - } - defer func() { - if err := accountDir.Close(); err != nil { - log.Warnf("Cannot close account directory") - } - }() - fileNames, err := accountDir.Readdirnames(0) - if err != nil { - // fileNames might be partially set, so log an error and - // at least try to open some accounts. - log.Errorf("Unable to read all account files: %v", err) - } - var accountNames []string - for _, file := range fileNames { - if strings.HasSuffix(file, "-wallet.bin") { - name := strings.TrimSuffix(file, "-wallet.bin") - accountNames = append(accountNames, name) - } - } - - // Open all additional accounts. - for _, acctName := range accountNames { - // Log txstore/utxostore errors as these will be recovered - // from with a rescan, but wallet errors must be returned - // to the caller. - a, err := openSavedAccount(acctName, cfg) - if err != nil { - switch err.(type) { - case *walletOpenError: - log.Errorf("Error opening account's wallet: %v", err) - - default: - log.Warnf("Non-critical error opening an account file: %v", err) - } - } else { - ad.addAccount(a) - } - } - return ad -} - -// accountHandler maintains accounts and structures for quick lookups for -// account information. Access to these structures must be requested via -// cmdChan. cmdChan is a single channel for multiple command types since there -// is ordering inherent in the commands and ordering between multipl goroutine -// reads via select{} is very much undefined. This function never returns and -// should be called as a new goroutine. -func (am *AccountManager) accountHandler() { - ad := openAccounts() - -out: - for { - select { - case c := <-am.cmdChan: - switch cmd := c.(type) { - case *openAccountsCmd: - // Write all old accounts before proceeding. - for _, a := range ad.nameToAccount { - if err := am.ds.FlushAccount(a); err != nil { - log.Errorf("Cannot write previously "+ - "scheduled account file: %v", err) - } - } - - ad = openAccounts() - case *accessAccountRequest: - a, ok := ad.nameToAccount[cmd.name] - if !ok { - a = nil - } - cmd.resp <- a - - case *accessAccountByAddressRequest: - a, ok := ad.addressToAccount[cmd.address] - if !ok { - a = nil - } - cmd.resp <- a - - case *accessAllRequest: - s := make([]*Account, 0, len(ad.nameToAccount)) - for _, a := range ad.nameToAccount { - s = append(s, a) - } - cmd.resp <- s - - case *addAccountCmd: - ad.addAccount(cmd.a) - case *removeAccountCmd: - ad.removeAccount(cmd.a) - - case *markAddressForAccountCmd: - // TODO(oga) make sure we own account - ad.addressToAccount[cmd.address] = cmd.account - } - - case <-am.quit: - break out - } - } - am.wg.Done() -} - -// keystoreLocker manages the lockedness state of all account keystores. -func (am *AccountManager) keystoreLocker() { - unlocked := false - var timeout <-chan time.Time -out: - for { - select { - case req := <-am.unlockRequests: - for _, a := range am.AllAccounts() { - if err := a.Unlock(req.passphrase); err != nil { - req.err <- err - continue out - } - } - unlocked = true - if req.timeout == 0 { - timeout = nil - } else { - timeout = time.After(req.timeout) - } - req.err <- nil - continue - - case am.unlockedState <- unlocked: - continue - - case <-am.quit: - break out - - case <-am.lockRequests: - case <-timeout: - } - - // Select statement fell through by an explicit lock or the - // timer expiring. Lock the keystores here. - timeout = nil - for _, a := range am.AllAccounts() { - if err := a.Lock(); err != nil { - log.Errorf("Could not lock wallet for account '%s': %v", - a.name, err) - } - } - unlocked = false - } - am.wg.Done() -} - -// rescanListener listens for messages from the rescan manager and marks -// accounts and addresses as synced. -func (am *AccountManager) rescanListener() { - for msg := range am.rescanMsgs { - AcctMgr.Grab() - switch e := msg.(type) { - case *RescanStartedMsg: - // Log the newly-started rescan. - n := 0 - for _, addrs := range e.Addresses { - n += len(addrs) - } - noun := pickNoun(n, "address", "addresses") - log.Infof("Started rescan at height %d for %d %s", e.StartHeight, n, noun) - - case *RescanProgressMsg: - for acct, addrs := range e.Addresses { - for i := range addrs { - err := acct.KeyStore.SetSyncStatus(addrs[i], - keystore.PartialSync(e.Height)) - if err != nil { - log.Errorf("Error marking address partially synced: %v", err) - continue - } - } - am.ds.ScheduleWalletWrite(acct) - err := am.ds.FlushAccount(acct) - if err != nil { - log.Errorf("Could not write rescan progress: %v", err) - } - } - - log.Infof("Rescanned through block height %d", e.Height) - - case *RescanFinishedMsg: - if e.Error != nil { - log.Errorf("Rescan failed: %v", e.Error) - break - } - - n := 0 - for acct, addrs := range e.Addresses { - n += len(addrs) - for i := range addrs { - err := acct.KeyStore.SetSyncStatus(addrs[i], - keystore.FullSync{}) - if err != nil { - log.Errorf("Error marking address synced: %v", err) - continue - } - } - am.ds.ScheduleWalletWrite(acct) - err := am.ds.FlushAccount(acct) - if err != nil { - log.Errorf("Could not write rescan progress: %v", err) - } - } - - noun := pickNoun(n, "address", "addresses") - log.Infof("Finished rescan for %d %s", n, noun) - - default: - // Unexpected rescan message type. - panic(e) - } - AcctMgr.Release() - } - am.wg.Done() -} - -// Grab grabs the account manager's binary semaphore. A custom semaphore -// is used instead of a sync.Mutex so the account manager's disk syncer -// can grab the semaphore from a select statement. -func (am *AccountManager) Grab() { - <-am.bsem -} - -// Release releases exclusive ownership of the AccountManager. -func (am *AccountManager) Release() { - am.bsem <- struct{}{} -} - -// OpenAccounts triggers the manager to reopen all known accounts. -func (am *AccountManager) OpenAccounts() { - am.cmdChan <- &openAccountsCmd{} -} - -// Account returns the account specified by name, or ErrNotFound -// as an error if the account is not found. -func (am *AccountManager) Account(name string) (*Account, error) { - respChan := make(chan *Account) - am.cmdChan <- &accessAccountRequest{ - name: name, - resp: respChan, - } - resp := <-respChan - if resp == nil { - return nil, ErrNotFound - } - return resp, nil -} - -// AccountByAddress returns the account specified by address, or -// ErrNotFound as an error if the account is not found. -func (am *AccountManager) AccountByAddress(addr btcutil.Address) (*Account, error) { - respChan := make(chan *Account) - req := accessAccountByAddressRequest{ - address: addr.EncodeAddress(), - resp: respChan, - } - select { - case am.cmdChan <- &req: - resp := <-respChan - if resp == nil { - return nil, ErrNotFound - } - return resp, nil - case <-am.quit: - return nil, ErrNoAccounts - } -} - -// MarkAddressForAccount labels the given account as containing the provided -// address. -func (am *AccountManager) MarkAddressForAccount(address btcutil.Address, - account *Account) { - // TODO(oga) really this entire dance should be carried out implicitly - // instead of requiring explicit messaging from the account to the - // manager. - req := markAddressForAccountCmd{ - address: address.EncodeAddress(), - account: account, - } - select { - case am.cmdChan <- &req: - case <-am.quit: - } -} - -// Address looks up an address if it is known to wallet at all. -func (am *AccountManager) Address(addr btcutil.Address) (keystore.WalletAddress, error) { - a, err := am.AccountByAddress(addr) - if err != nil { - return nil, err - } - - return a.KeyStore.Address(addr) -} - -// AllAccounts returns a slice of all managed accounts. -func (am *AccountManager) AllAccounts() []*Account { - respChan := make(chan []*Account) - req := accessAllRequest{ - resp: respChan, - } - select { - case am.cmdChan <- &req: - return <-respChan - case <-am.quit: - return nil - } -} - -// AddAccount adds an account to the collection managed by an AccountManager. -func (am *AccountManager) AddAccount(a *Account) { - req := addAccountCmd{ - a: a, - } - select { - case am.cmdChan <- &req: - case <-am.quit: - } -} - -// RemoveAccount removes an account to the collection managed by an -// AccountManager. -func (am *AccountManager) RemoveAccount(a *Account) { - req := removeAccountCmd{ - a: a, - } - select { - case am.cmdChan <- &req: - case <-am.quit: - } -} - -// RegisterNewAccount adds a new memory account to the account manager, -// and immediately writes the account to disk. -func (am *AccountManager) RegisterNewAccount(a *Account) error { - am.AddAccount(a) - - // Ensure that the new account is written out to disk. - am.ds.ScheduleWalletWrite(a) - am.ds.ScheduleTxStoreWrite(a) - if err := am.ds.FlushAccount(a); err != nil { - am.RemoveAccount(a) - return err - } - return nil -} - -// Rollback rolls back each managed Account to the state before the block -// specified by height and hash was connected to the main chain. -func (am *AccountManager) Rollback(height int32, hash *btcwire.ShaHash) error { - for _, a := range am.AllAccounts() { - if err := a.TxStore.Rollback(height); err != nil { - return err - } - am.ds.ScheduleTxStoreWrite(a) - } - return nil -} - -// BlockNotify notifies all wallet clients of any changes from the new block, -// including changed balances. Each account is then set to be synced -// with the latest block. -func (am *AccountManager) BlockNotify(bs *keystore.BlockStamp) { - for _, a := range am.AllAccounts() { - // TODO: need a flag or check that the utxo store was actually - // modified, or this will notify even if there are no balance - // changes, or sending these notifications as the utxos are added. - confirmed, err := a.CalculateBalance(1) - var unconfirmed btcutil.Amount - if err == nil { - unconfirmed, err = a.CalculateBalance(0) - } - if err == nil { - unconfirmed -= confirmed - server.NotifyWalletBalance(a.name, confirmed) - server.NotifyWalletBalanceUnconfirmed(a.name, unconfirmed) - } - - // If this is the default account, update the block all accounts - // are synced with, and schedule a wallet write. - if a.KeyStore.Name() == "" { - a.KeyStore.SetSyncedWith(bs) - am.ds.ScheduleWalletWrite(a) - } - } -} - -// RecordMinedTx searches through each account's TxStore, searching for a -// sent transaction with the same txid as from a txmined notification. If -// the transaction IDs match, the record in the TxStore is updated with -// the full information about the newly-mined tx, and the TxStore is -// scheduled to be written to disk.. -func (am *AccountManager) RecordSpendingTx(tx *btcutil.Tx, block *txstore.Block) error { - for _, a := range am.AllAccounts() { - // TODO(jrick): This needs to iterate through each txout's - // addresses and find whether this account's keystore contains - // any of the addresses this tx sends to. - txr, err := a.TxStore.InsertTx(tx, block) - if err != nil { - return err - } - // When received as a notification, we don't know what the inputs are. - if _, err := txr.AddDebits(nil); err != nil { - return err - } - am.ds.ScheduleTxStoreWrite(a) - } - return nil -} - -// CalculateBalance returns the balance, calculated using minconf block -// confirmations, of an account. -func (am *AccountManager) CalculateBalance(account string, minconf int) (btcutil.Amount, error) { - a, err := am.Account(account) - if err != nil { - return 0, err - } - - return a.CalculateBalance(minconf) -} - -// CreateEncryptedWallet creates a new default account with a wallet file -// encrypted with passphrase. -func (am *AccountManager) CreateEncryptedWallet(passphrase []byte) error { - if len(am.AllAccounts()) != 0 { - return ErrWalletExists - } - - // Get current block's height and hash. - rpcc, err := accessClient() - if err != nil { - return err - } - bs, err := rpcc.BlockStamp() - if err != nil { - return err - } - - // Create new wallet in memory. - keys, err := keystore.NewStore("", "Default acccount", passphrase, - activeNet.Params, &bs, cfg.KeypoolSize) - if err != nil { - return err - } - - // Create new account and begin managing with the global account - // manager. Registering will fail if the new account can not be - // written immediately to disk. - a := newAccount("", keys, txstore.New()) - if err := am.RegisterNewAccount(a); err != nil { - return err - } - - // Begin tracking account against a connected btcd. - a.Track() - - return nil -} - -// ChangePassphrase unlocks all account wallets with the old -// passphrase, and re-encrypts each using the new passphrase. -func (am *AccountManager) ChangePassphrase(old, new []byte) error { - // Keystores must be unlocked to change their passphrase. - err := am.UnlockWallets(old, 0) - if err != nil { - return err - } - - accts := am.AllAccounts() - - // Change passphrase for each unlocked wallet. - for _, a := range accts { - err = a.KeyStore.ChangePassphrase(new) - if err != nil { - return err - } - } - - am.LockWallets() - - // Immediately write out to disk. - return am.ds.WriteBatch(accts) -} - -// LockWallets locks all managed account wallets. -func (am *AccountManager) LockWallets() { - am.lockRequests <- struct{}{} -} - -// UnlockWallets unlocks all managed account's wallets, locking them again after -// the timeout expires, or resetting a previous timeout if one is still running. -func (am *AccountManager) UnlockWallets(passphrase []byte, timeout time.Duration) error { - req := unlockRequest{ - passphrase: passphrase, - timeout: timeout, - err: make(chan error, 1), - } - am.unlockRequests <- req - return <-req.err -} - -// DumpKeys returns all WIF-encoded private keys associated with all -// accounts. All wallets must be unlocked for this operation to succeed. -func (am *AccountManager) DumpKeys() ([]string, error) { - keys := []string{} - for _, a := range am.AllAccounts() { - switch walletKeys, err := a.DumpPrivKeys(); err { - case keystore.ErrLocked: - return nil, err - - case nil: - keys = append(keys, walletKeys...) - - default: // any other non-nil error - return nil, err - } - - } - return keys, nil -} - -// DumpWIFPrivateKey searches through all accounts for the bitcoin -// payment address addr and returns the WIF-encdoded private key. -func (am *AccountManager) DumpWIFPrivateKey(addr btcutil.Address) (string, error) { - a, err := am.AccountByAddress(addr) - if err != nil { - return "", err - } - return a.DumpWIFPrivateKey(addr) -} - -// ListAccounts returns a map of account names to their current account -// balances. The balances are calculated using minconf confirmations. -func (am *AccountManager) ListAccounts(minconf int) (map[string]btcutil.Amount, error) { - // Create and fill a map of account names and their balances. - accts := am.AllAccounts() - pairs := make(map[string]btcutil.Amount, len(accts)) - for _, a := range accts { - bal, err := a.CalculateBalance(minconf) - if err != nil { - return nil, err - } - pairs[a.name] = bal - } - return pairs, nil -} - -// ListAccountsF64 returns a map of account names to their current account -// balances. The balances are calculated using minconf confirmations. -// -// The amounts are converted to float64 so this result may be marshaled -// as a JSON object for the listaccounts RPC. -func (am *AccountManager) ListAccountsF64(minconf int) (map[string]float64, error) { - // Create and fill a map of account names and their balances. - accts := am.AllAccounts() - pairs := make(map[string]float64, len(accts)) - for _, a := range accts { - bal, err := a.CalculateBalance(minconf) - if err != nil { - return nil, err - } - pairs[a.name] = bal.ToUnit(btcutil.AmountBTC) - } - return pairs, nil -} - -// ListSinceBlock returns a slice of objects representing all transactions in -// the wallets since the given block. -// To be used for the listsinceblock command. -func (am *AccountManager) ListSinceBlock(since, curBlockHeight int32, - minconf int) ([]btcjson.ListTransactionsResult, error) { - - // Create and fill a map of account names and their balances. - txList := []btcjson.ListTransactionsResult{} - for _, a := range am.AllAccounts() { - txTmp, err := a.ListSinceBlock(since, curBlockHeight, minconf) - if err != nil { - return nil, err - } - txList = append(txList, txTmp...) - } - return txList, nil -} - -// accountTx represents an account/transaction pair to be used by -// GetTransaction. -type accountTx struct { - Account string - Tx *txstore.TxRecord -} - -// GetTransaction returns an array of accountTx to fully represent the effect of -// a transaction on locally known wallets. If we know nothing about a -// transaction an empty array will be returned. -func (am *AccountManager) GetTransaction(txSha *btcwire.ShaHash) []accountTx { - accumulatedTxen := []accountTx{} - - for _, a := range am.AllAccounts() { - for _, record := range a.TxStore.Records() { - if *record.Tx().Sha() != *txSha { - continue - } - - atx := accountTx{ - Account: a.name, - Tx: record, - } - accumulatedTxen = append(accumulatedTxen, atx) - } - } - - return accumulatedTxen -} - -// ListUnspent returns a slice of objects representing the unspent wallet -// transactions fitting the given criteria. The confirmations will be more than -// minconf, less than maxconf and if addresses is populated only the addresses -// contained within it will be considered. If we know nothing about a -// transaction an empty array will be returned. -func (am *AccountManager) ListUnspent(minconf, maxconf int, - addresses map[string]bool) ([]*btcjson.ListUnspentResult, error) { - - results := []*btcjson.ListUnspentResult{} - - rpcc, err := accessClient() - if err != nil { - return results, err - } - bs, err := rpcc.BlockStamp() - if err != nil { - return results, err - } - - filter := len(addresses) != 0 - - for _, a := range am.AllAccounts() { - unspent, err := a.TxStore.SortedUnspentOutputs() - if err != nil { - return nil, err - } - - for _, credit := range unspent { - confs := credit.Confirmations(bs.Height) - if int(confs) < minconf || int(confs) > maxconf { - continue - } - if credit.IsCoinbase() { - if !credit.Confirmed(btcchain.CoinbaseMaturity, bs.Height) { - continue - } - } - if a.LockedOutpoint(*credit.OutPoint()) { - continue - } - - _, addrs, _, _ := credit.Addresses(activeNet.Params) - if filter { - for _, addr := range addrs { - _, ok := addresses[addr.EncodeAddress()] - if ok { - goto include - } - } - continue - } - include: - result := &btcjson.ListUnspentResult{ - TxId: credit.Tx().Sha().String(), - Vout: credit.OutputIndex, - Account: a.KeyStore.Name(), - ScriptPubKey: hex.EncodeToString(credit.TxOut().PkScript), - Amount: credit.Amount().ToUnit(btcutil.AmountBTC), - Confirmations: int64(confs), - } - - // BUG: this should be a JSON array so that all - // addresses can be included, or removed (and the - // caller extracts addresses from the pkScript). - if len(addrs) > 0 { - result.Address = addrs[0].EncodeAddress() - } - - results = append(results, result) - } - } - - return results, nil -} - -// RescanActiveAddresses begins a rescan for all active addresses for -// each account. If markBestBlock is non-nil, the block described by -// the blockstamp is used to mark the synced-with height of the wallet -// just before the rescan is submitted and started. This allows the -// caller to mark the progress that the rescan is expected to complete -// through, if the account otherwise does not contain any recently -// seen blocks. -func (am *AccountManager) RescanActiveAddresses(markBestBlock *keystore.BlockStamp) error { - var job *RescanJob - var defaultAcct *Account - for _, a := range am.AllAccounts() { - acctJob, err := a.RescanActiveJob() - if err != nil { - return err - } - if job == nil { - job = acctJob - } else { - job.Merge(acctJob) - } - - if a.name == "" { - defaultAcct = a - } - } - if job != nil { - if markBestBlock != nil { - defaultAcct.KeyStore.SetSyncedWith(markBestBlock) - } - - // Submit merged job and block until rescan completes. - jobFinished := am.rm.SubmitJob(job) - <-jobFinished - } - - return nil -} - -func (am *AccountManager) ResendUnminedTxs() { - for _, a := range am.AllAccounts() { - a.ResendUnminedTxs() - } -} - -// Track begins tracking all addresses in all accounts for updates from -// btcd. -func (am *AccountManager) Track() { - for _, a := range am.AllAccounts() { - a.Track() - } -} diff --git a/btcwallet.go b/btcwallet.go new file mode 100644 index 0000000..47ae938 --- /dev/null +++ b/btcwallet.go @@ -0,0 +1,168 @@ +/* + * Copyright (c) 2013, 2014 Conformal Systems LLC + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +package main + +import ( + "io/ioutil" + "net" + "net/http" + _ "net/http/pprof" + "os" + "runtime" + + "github.com/conformal/btcwallet/chain" +) + +var ( + cfg *config + shutdownChan = make(chan struct{}) +) + +func main() { + // Use all processor cores. + runtime.GOMAXPROCS(runtime.NumCPU()) + + // Work around defer not working after os.Exit. + if err := walletMain(); err != nil { + os.Exit(1) + } +} + +// walletMain is a work-around main function that is required since deferred +// functions (such as log flushing) are not called with calls to os.Exit. +// Instead, main runs this function and checks for a non-nil error, at which +// point any defers have already run, and if the error is non-nil, the program +// can be exited with an error exit status. +func walletMain() error { + // Load configuration and parse command line. This function also + // initializes logging and configures it accordingly. + tcfg, _, err := loadConfig() + if err != nil { + return err + } + cfg = tcfg + defer backendLog.Flush() + + if cfg.Profile != "" { + go func() { + listenAddr := net.JoinHostPort("", cfg.Profile) + log.Infof("Profile server listening on %s", listenAddr) + profileRedirect := http.RedirectHandler("/debug/pprof", + http.StatusSeeOther) + http.Handle("/", profileRedirect) + log.Errorf("%v", http.ListenAndServe(listenAddr, nil)) + }() + } + + // Create and start HTTP server to serve wallet client connections. + // This will be updated with the wallet and chain server RPC client + // created below after each is created. + server, err := newRPCServer(cfg.SvrListeners, cfg.RPCMaxClients, + cfg.RPCMaxWebsockets) + if err != nil { + log.Errorf("Unable to create HTTP server: %v", err) + return err + } + server.Start() + + // Shutdown the server if an interrupt signal is received. + addInterruptHandler(server.Stop) + + // Create channel so that the goroutine which opens the chain server + // connection can pass the conn to the goroutine which opens the wallet. + // Buffer the channel so sends are not blocked, since if the wallet is + // not yet created, the wallet open goroutine does not read this. + chainSvrChan := make(chan *chain.Client, 1) + + go func() { + // Read CA certs and create the RPC client. + certs, err := ioutil.ReadFile(cfg.CAFile) + if err != nil { + log.Errorf("Cannot open CA file: %v", err) + close(chainSvrChan) + return + } + rpcc, err := chain.NewClient(activeNet.Params, cfg.RPCConnect, + cfg.Username, cfg.Password, certs) + if err != nil { + log.Errorf("Cannot create chain server RPC client: %v", err) + close(chainSvrChan) + return + } + err = rpcc.Start() + if err != nil { + log.Warnf("Connection to Bitcoin RPC chain server " + + "unsuccessful -- available RPC methods will be limited") + } + // Even if Start errored, we still add the server disconnected. + // All client methods will then error, so it's obvious to a + // client that the there was a connection problem. + server.SetChainServer(rpcc) + + chainSvrChan <- rpcc + }() + + // Create a channel to report unrecoverable errors during the loading of + // the wallet files. These may include OS file handling errors or + // issues deserializing the wallet files, but does not include missing + // wallet files (as that must be handled by creating a new wallet). + walletOpenErrors := make(chan error) + + go func() { + defer close(walletOpenErrors) + + // Open wallet structures from disk. + w, err := openWallet() + if err != nil { + if os.IsNotExist(err) { + // If the keystore file is missing, notify the server + // that generating new wallets is ok. + server.SetWallet(nil) + return + } else { + // If the keystore file exists but another error was + // encountered, we cannot continue. + log.Errorf("Cannot load wallet files: %v", err) + walletOpenErrors <- err + return + } + } + + server.SetWallet(w) + + // Start wallet goroutines and handle RPC client notifications + // if the chain server connection was opened. + select { + case chainSvr := <-chainSvrChan: + w.Start(chainSvr) + case <-server.quit: + } + }() + + // Check for unrecoverable errors during the wallet startup, and return + // the error, if any. + err, ok := <-walletOpenErrors + if ok { + return err + } + + // Wait for the server to shutdown either due to a stop RPC request + // or an interrupt. + server.WaitForShutdown() + log.Info("Shutdown complete") + return nil +} diff --git a/chain/chain.go b/chain/chain.go new file mode 100644 index 0000000..68776c1 --- /dev/null +++ b/chain/chain.go @@ -0,0 +1,306 @@ +/* + * Copyright (c) 2013, 2014 Conformal Systems LLC + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +package chain + +import ( + "errors" + "sync" + "time" + + "github.com/conformal/btcnet" + "github.com/conformal/btcrpcclient" + "github.com/conformal/btcutil" + "github.com/conformal/btcwallet/keystore" + "github.com/conformal/btcwallet/txstore" + "github.com/conformal/btcwire" + "github.com/conformal/btcws" +) + +type Client struct { + *btcrpcclient.Client + netParams *btcnet.Params + + enqueueNotification chan interface{} + dequeueNotification chan interface{} + currentBlock chan *keystore.BlockStamp + + quit chan struct{} + wg sync.WaitGroup + started bool + quitMtx sync.Mutex +} + +func NewClient(net *btcnet.Params, connect, user, pass string, certs []byte) (*Client, error) { + client := Client{ + netParams: net, + enqueueNotification: make(chan interface{}), + dequeueNotification: make(chan interface{}), + currentBlock: make(chan *keystore.BlockStamp), + quit: make(chan struct{}), + } + initializedClient := make(chan struct{}) + ntfnCallbacks := btcrpcclient.NotificationHandlers{ + OnClientConnected: func() { + log.Info("Established connection to btcd") + }, + OnBlockConnected: client.onBlockConnected, + OnBlockDisconnected: client.onBlockDisconnected, + OnRecvTx: client.onRecvTx, + OnRedeemingTx: client.onRedeemingTx, + OnRescanFinished: client.onRescanFinished, + OnRescanProgress: client.onRescanProgress, + } + conf := btcrpcclient.ConnConfig{ + Host: connect, + Endpoint: "ws", + User: user, + Pass: pass, + Certificates: certs, + DisableConnectOnNew: true, + } + c, err := btcrpcclient.New(&conf, &ntfnCallbacks) + if err != nil { + return nil, err + } + client.Client = c + close(initializedClient) + return &client, nil +} + +func (c *Client) Start() error { + err := c.Connect(5) // attempt connection 5 tries at most + if err != nil { + return err + } + + // Verify that the server is running on the expected network. + net, err := c.GetCurrentNet() + if err != nil { + c.Disconnect() + return err + } + if net != c.netParams.Net { + c.Disconnect() + return errors.New("mismatched networks") + } + + c.quitMtx.Lock() + c.started = true + c.quitMtx.Unlock() + + c.wg.Add(1) + go c.handler() + return nil +} + +func (c *Client) Stop() { + c.quitMtx.Lock() + defer c.quitMtx.Unlock() + + select { + case <-c.quit: + default: + close(c.quit) + c.Client.Shutdown() + + if !c.started { + close(c.dequeueNotification) + } + } +} + +func (c *Client) WaitForShutdown() { + c.Client.WaitForShutdown() + c.wg.Wait() +} + +func (c *Client) Notifications() <-chan interface{} { + return c.dequeueNotification +} + +func (c *Client) BlockStamp() (*keystore.BlockStamp, error) { + select { + case bs := <-c.currentBlock: + return bs, nil + case <-c.quit: + return nil, errors.New("disconnected") + } +} + +// Notification types. These are defined here and processed from from reading +// a notificationChan to avoid handling these notifications directly in +// btcrpcclient callbacks, which isn't very Go-like and doesn't allow +// blocking client calls. +type ( + BlockConnected keystore.BlockStamp + BlockDisconnected keystore.BlockStamp + RecvTx struct { + Tx *btcutil.Tx // Index is guaranteed to be set. + Block *txstore.Block // nil if unmined + } + RedeemingTx struct { + Tx *btcutil.Tx // Index is guaranteed to be set. + Block *txstore.Block // nil if unmined + } + RescanProgress struct { + Hash *btcwire.ShaHash + Height int32 + Time time.Time + } + RescanFinished struct { + Hash *btcwire.ShaHash + Height int32 + Time time.Time + } +) + +// parseBlock parses a btcws definition of the block a tx is mined it to the +// Block structure of the txstore package, and the block index. This is done +// here since btcrpcclient doesn't parse this nicely for us. +func parseBlock(block *btcws.BlockDetails) (blk *txstore.Block, idx int, err error) { + if block == nil { + return nil, btcutil.TxIndexUnknown, nil + } + blksha, err := btcwire.NewShaHashFromStr(block.Hash) + if err != nil { + return nil, btcutil.TxIndexUnknown, err + } + blk = &txstore.Block{ + Height: block.Height, + Hash: *blksha, + Time: time.Unix(block.Time, 0), + } + return blk, block.Index, nil +} + +func (c *Client) onBlockConnected(hash *btcwire.ShaHash, height int32) { + c.enqueueNotification <- BlockConnected{Hash: hash, Height: height} +} + +func (c *Client) onBlockDisconnected(hash *btcwire.ShaHash, height int32) { + c.enqueueNotification <- BlockDisconnected{Hash: hash, Height: height} +} + +func (c *Client) onRecvTx(tx *btcutil.Tx, block *btcws.BlockDetails) { + var blk *txstore.Block + index := btcutil.TxIndexUnknown + if block != nil { + var err error + blk, index, err = parseBlock(block) + if err != nil { + // Log and drop improper notification. + log.Errorf("recvtx notification bad block: %v", err) + return + } + } + tx.SetIndex(index) + c.enqueueNotification <- RecvTx{tx, blk} +} + +func (c *Client) onRedeemingTx(tx *btcutil.Tx, block *btcws.BlockDetails) { + var blk *txstore.Block + index := btcutil.TxIndexUnknown + if block != nil { + var err error + blk, index, err = parseBlock(block) + if err != nil { + // Log and drop improper notification. + log.Errorf("recvtx notification bad block: %v", err) + return + } + } + tx.SetIndex(index) + c.enqueueNotification <- RedeemingTx{tx, blk} +} + +func (c *Client) onRescanProgress(hash *btcwire.ShaHash, height int32, blkTime time.Time) { + c.enqueueNotification <- &RescanProgress{hash, height, blkTime} +} + +func (c *Client) onRescanFinished(hash *btcwire.ShaHash, height int32, blkTime time.Time) { + c.enqueueNotification <- &RescanFinished{hash, height, blkTime} +} + +// handler maintains a queue of notifications and the current state (best +// block) of the chain. +func (c *Client) handler() { + hash, height, err := c.GetBestBlock() + if err != nil { + close(c.quit) + c.wg.Done() + } + + bs := &keystore.BlockStamp{Hash: hash, Height: height} + + // TODO: Rather than leaving this as an unbounded queue for all types of + // notifications, try dropping ones where a later enqueued notification + // can fully invalidate one waiting to be processed. For example, + // blockconnected notifications for greater block heights can remove the + // need to process earlier blockconnected notifications still waiting + // here. + + var notifications []interface{} + enqueue := c.enqueueNotification + var dequeue chan interface{} + var next interface{} +out: + for { + select { + case n, ok := <-enqueue: + if !ok { + // If no notifications are queued for handling, + // the queue is finished. + if len(notifications) == 0 { + break out + } + // nil channel so no more reads can occur. + enqueue = nil + continue + } + if len(notifications) == 0 { + next = n + dequeue = c.dequeueNotification + } + notifications = append(notifications, n) + + case dequeue <- next: + if n, ok := next.(BlockConnected); ok { + bs = (*keystore.BlockStamp)(&n) + } + + notifications[0] = nil + notifications = notifications[1:] + if len(notifications) != 0 { + next = notifications[0] + } else { + // If no more notifications can be enqueued, the + // queue is finished. + if enqueue == nil { + break out + } + dequeue = nil + } + + case c.currentBlock <- bs: + + case <-c.quit: + break out + } + } + close(c.dequeueNotification) + c.wg.Done() +} diff --git a/chain/log.go b/chain/log.go new file mode 100644 index 0000000..0bf9092 --- /dev/null +++ b/chain/log.go @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2013, 2014 Conformal Systems LLC + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +package chain + +import "github.com/conformal/btclog" + +// log is a logger that is initialized with no output filters. This +// means the package will not perform any logging by default until the caller +// requests it. +var log btclog.Logger + +// The default amount of logging is none. +func init() { + DisableLog() +} + +// DisableLog disables all library log output. Logging output is disabled +// by default until either UseLogger or SetLogWriter are called. +func DisableLog() { + log = btclog.Disabled +} + +// UseLogger uses a specified Logger to output package logging info. +// This should be used in preference to SetLogWriter if the caller is also +// using btclog. +func UseLogger(logger btclog.Logger) { + log = logger +} + +// LogClosure is a closure that can be printed with %v to be used to +// generate expensive-to-create data for a detailed log level and avoid doing +// the work if the data isn't printed. +type logClosure func() string + +// String invokes the log closure and returns the results string. +func (c logClosure) String() string { + return c() +} + +// newLogClosure returns a new closure over the passed function which allows +// it to be used as a parameter in a logging function that is only invoked when +// the logging level is such that the message will actually be logged. +func newLogClosure(c func() string) logClosure { + return logClosure(c) +} + +// pickNoun returns the singular or plural form of a noun depending +// on the count n. +func pickNoun(n int, singular, plural string) string { + if n == 1 { + return singular + } + return plural +} diff --git a/chainntfns.go b/chainntfns.go new file mode 100644 index 0000000..07d706b --- /dev/null +++ b/chainntfns.go @@ -0,0 +1,182 @@ +/* + * Copyright (c) 2013, 2014 Conformal Systems LLC + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +package main + +import ( + "github.com/conformal/btcscript" + "github.com/conformal/btcutil" + "github.com/conformal/btcwallet/chain" + "github.com/conformal/btcwallet/keystore" + "github.com/conformal/btcwallet/txstore" +) + +func (w *Wallet) handleChainNotifications() { + for n := range w.chainSvr.Notifications() { + var err error + switch n := n.(type) { + case chain.BlockConnected: + w.connectBlock(keystore.BlockStamp(n)) + case chain.BlockDisconnected: + w.disconnectBlock(keystore.BlockStamp(n)) + case chain.RecvTx: + err = w.addReceivedTx(n.Tx, n.Block) + case chain.RedeemingTx: + err = w.addRedeemingTx(n.Tx, n.Block) + + // The following are handled by the wallet's rescan + // goroutines, so just pass them there. + case *chain.RescanProgress, *chain.RescanFinished: + w.rescanNotifications <- n + } + if err != nil { + log.Errorf("Cannot handle chain server "+ + "notification: %v", err) + } + } + w.wg.Done() +} + +// connectBlock handles a chain server notification by marking a wallet +// that's currently in-sync with the chain server as being synced up to +// the passed block. +func (w *Wallet) connectBlock(bs keystore.BlockStamp) { + if !w.ChainSynced() { + return + } + + w.KeyStore.SetSyncedWith(&bs) + w.KeyStore.MarkDirty() + w.notifyConnectedBlock(bs) + + w.notifyBalances(bs.Height) +} + +// disconnectBlock handles a chain server reorganize by rolling back all +// block history from the reorged block for a wallet in-sync with the chain +// server. +func (w *Wallet) disconnectBlock(bs keystore.BlockStamp) { + if !w.ChainSynced() { + return + } + + // Disconnect the last seen block from the keystore if it + // matches the removed block. + iter := w.KeyStore.NewIterateRecentBlocks() + if iter != nil && *iter.BlockStamp().Hash == *bs.Hash { + if iter.Prev() { + prev := iter.BlockStamp() + w.KeyStore.SetSyncedWith(&prev) + } else { + w.KeyStore.SetSyncedWith(nil) + } + w.KeyStore.MarkDirty() + } + w.notifyDisconnectedBlock(bs) + + w.notifyBalances(bs.Height - 1) +} + +func (w *Wallet) addReceivedTx(tx *btcutil.Tx, block *txstore.Block) error { + // For every output, if it pays to a wallet address, insert the + // transaction into the store (possibly moving it from unconfirmed to + // confirmed), and add a credit record if one does not already exist. + var txr *txstore.TxRecord + txInserted := false + for txOutIdx, txOut := range tx.MsgTx().TxOut { + // Errors don't matter here. If addrs is nil, the range below + // does nothing. + _, addrs, _, _ := btcscript.ExtractPkScriptAddrs(txOut.PkScript, + activeNet.Params) + insert := false + for _, addr := range addrs { + _, err := w.KeyStore.Address(addr) + if err == nil { + insert = true + break + } + } + if insert { + if !txInserted { + var err error + txr, err = w.TxStore.InsertTx(tx, block) + if err != nil { + return err + } + // InsertTx may have moved a previous unmined + // tx, so mark the entire store as dirty. + w.TxStore.MarkDirty() + txInserted = true + } + if txr.HasCredit(txOutIdx) { + continue + } + _, err := txr.AddCredit(uint32(txOutIdx), false) + if err != nil { + return err + } + w.TxStore.MarkDirty() + } + } + + bs, err := w.chainSvr.BlockStamp() + if err == nil { + w.notifyBalances(bs.Height) + } + + return nil +} + +// addRedeemingTx inserts the notified spending transaction as a debit and +// schedules the transaction store for a future file write. +func (w *Wallet) addRedeemingTx(tx *btcutil.Tx, block *txstore.Block) error { + txr, err := w.TxStore.InsertTx(tx, block) + if err != nil { + return err + } + if _, err := txr.AddDebits(); err != nil { + return err + } + w.KeyStore.MarkDirty() + + bs, err := w.chainSvr.BlockStamp() + if err == nil { + w.notifyBalances(bs.Height) + } + + return nil +} + +func (w *Wallet) notifyBalances(curHeight int32) { + // Don't notify unless wallet is synced to the chain server. + if !w.ChainSynced() { + return + } + + // Notify any potential changes to the balance. + confirmed, err := w.TxStore.Balance(1, curHeight) + if err != nil { + log.Errorf("Cannot determine 1-conf balance: %v", err) + return + } + w.notifyConfirmedBalance(confirmed) + unconfirmed, err := w.TxStore.Balance(0, curHeight) + if err != nil { + log.Errorf("Cannot determine 0-conf balance: %v", err) + return + } + w.notifyUnconfirmedBalance(unconfirmed - confirmed) +} diff --git a/cmd.go b/cmd.go deleted file mode 100644 index a0e5bd9..0000000 --- a/cmd.go +++ /dev/null @@ -1,161 +0,0 @@ -/* - * Copyright (c) 2013, 2014 Conformal Systems LLC - * - * Permission to use, copy, modify, and distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - */ - -package main - -import ( - "errors" - "io/ioutil" - "net" - "net/http" - _ "net/http/pprof" - "os" - "time" -) - -var ( - cfg *config - server *rpcServer - shutdownChan = make(chan struct{}) - clientAccessChan = make(chan *rpcClient) -) - -func clientAccess(newClient <-chan *rpcClient) { - var client *rpcClient - for { - select { - case c := <-newClient: - client = c - case clientAccessChan <- client: - } - } -} - -func accessClient() (*rpcClient, error) { - c := <-clientAccessChan - if c == nil { - return nil, errors.New("chain server disconnected") - } - return c, nil -} - -func clientConnect(certs []byte, newClient chan<- *rpcClient) { - const initialWait = 5 * time.Second - wait := initialWait - for { - select { - case <-server.quit: - return - default: - } - - client, err := newRPCClient(certs) - if err != nil { - log.Warnf("Unable to open chain server client "+ - "connection: %v", err) - time.Sleep(wait) - wait <<= 1 - if wait > time.Minute { - wait = time.Minute - } - continue - } - - wait = initialWait - client.Start() - newClient <- client - - client.WaitForShutdown() - } -} - -func main() { - // Work around defer not working after os.Exit. - if err := walletMain(); err != nil { - os.Exit(1) - } -} - -// walletMain is a work-around main function that is required since deferred -// functions (such as log flushing) are not called with calls to os.Exit. -// Instead, main runs this function and checks for a non-nil error, at which -// point any defers have already run, and if the error is non-nil, the program -// can be exited with an error exit status. -func walletMain() error { - // Load configuration and parse command line. This function also - // initializes logging and configures it accordingly. - tcfg, _, err := loadConfig() - if err != nil { - return err - } - cfg = tcfg - defer backendLog.Flush() - - if cfg.Profile != "" { - go func() { - listenAddr := net.JoinHostPort("", cfg.Profile) - log.Infof("Profile server listening on %s", listenAddr) - profileRedirect := http.RedirectHandler("/debug/pprof", - http.StatusSeeOther) - http.Handle("/", profileRedirect) - log.Errorf("%v", http.ListenAndServe(listenAddr, nil)) - }() - } - - // Read CA file to verify a btcd TLS connection. - certs, err := ioutil.ReadFile(cfg.CAFile) - if err != nil { - log.Errorf("cannot open CA file: %v", err) - return err - } - - // Check and update any old file locations. - err = updateOldFileLocations() - if err != nil { - return err - } - - // Start account manager and open accounts. - AcctMgr.Start() - - server, err = newRPCServer( - cfg.SvrListeners, - cfg.RPCMaxClients, - cfg.RPCMaxWebsockets, - ) - if err != nil { - log.Errorf("Unable to create HTTP server: %v", err) - return err - } - - // Start HTTP server to serve wallet client connections. - server.Start() - - // Shutdown the server if an interrupt signal is received. - addInterruptHandler(server.Stop) - - // Start client connection to a btcd chain server. Attempt - // reconnections if the client could not be successfully connected. - clientChan := make(chan *rpcClient) - go clientAccess(clientChan) - go clientConnect(certs, clientChan) - - // Wait for the server to shutdown either due to a stop RPC request - // or an interrupt. - server.WaitForShutdown() - log.Info("Shutdown complete") - return nil -} diff --git a/config.go b/config.go index 66e9a69..2f6654a 100644 --- a/config.go +++ b/config.go @@ -36,7 +36,6 @@ const ( defaultLogLevel = "info" defaultLogDirname = "logs" defaultLogFilename = "btcwallet.log" - defaultKeypoolSize = 100 defaultDisallowFree = false defaultRPCMaxClients = 10 defaultRPCMaxWebsockets = 25 @@ -72,7 +71,7 @@ type config struct { RPCMaxWebsockets int64 `long:"rpcmaxwebsockets" description:"Max number of RPC websocket connections"` MainNet bool `long:"mainnet" description:"Use the main Bitcoin network (default testnet3)"` SimNet bool `long:"simnet" description:"Use the simulation test network (default testnet3)"` - KeypoolSize uint `short:"k" long:"keypoolsize" description:"Maximum number of addresses in keypool"` + KeypoolSize uint `short:"k" long:"keypoolsize" description:"DEPRECATED -- Maximum number of addresses in keypool"` DisallowFree bool `long:"disallowfree" description:"Force transactions to always include a fee"` Proxy string `long:"proxy" description:"Connect via SOCKS5 proxy (eg. 127.0.0.1:9050)"` ProxyUser string `long:"proxyuser" description:"Username for proxy server"` @@ -243,7 +242,6 @@ func loadConfig() (*config, []string, error) { LogDir: defaultLogDir, RPCKey: defaultRPCKeyFile, RPCCert: defaultRPCCertFile, - KeypoolSize: defaultKeypoolSize, DisallowFree: defaultDisallowFree, RPCMaxClients: defaultRPCMaxClients, RPCMaxWebsockets: defaultRPCMaxWebsockets, diff --git a/createtx.go b/createtx.go index 415bdf3..c8e3f5d 100644 --- a/createtx.go +++ b/createtx.go @@ -65,9 +65,9 @@ var ErrNegativeFee = errors.New("fee is negative") const defaultFeeIncrement = 10000 type CreatedTx struct { - tx *btcutil.Tx - inputs []txstore.Credit - changeAddr btcutil.Address + tx *btcutil.Tx + changeAddr btcutil.Address + changeIndex int // negative if no change } // ByAmount defines the methods needed to satisify sort.Interface to @@ -114,13 +114,17 @@ func selectInputs(eligible []txstore.Credit, amt, fee btcutil.Amount, // address, changeUtxo will point to a unconfirmed (height = -1, zeroed // block hash) Utxo. ErrInsufficientFunds is returned if there are not // enough eligible unspent outputs to create the transaction. -func (a *Account) txToPairs(pairs map[string]btcutil.Amount, +func (w *Wallet) txToPairs(pairs map[string]btcutil.Amount, minconf int) (*CreatedTx, error) { - // Wallet must be unlocked to compose transaction. - if a.KeyStore.IsLocked() { - return nil, keystore.ErrLocked + // Key store must be unlocked to compose transaction. Grab the + // unlock if possible (to prevent future unlocks), or return the + // error if the keystore is already locked. + heldUnlock, err := w.HoldUnlock() + if err != nil { + return nil, err } + defer heldUnlock.Release() // Create a new transaction which will include all input scripts. msgtx := btcwire.NewMsgTx() @@ -152,11 +156,7 @@ func (a *Account) txToPairs(pairs map[string]btcutil.Amount, } // Get current block's height and hash. - rpcc, err := accessClient() - if err != nil { - return nil, err - } - bs, err := rpcc.BlockStamp() + bs, err := w.chainSvr.BlockStamp() if err != nil { return nil, err } @@ -166,7 +166,7 @@ func (a *Account) txToPairs(pairs map[string]btcutil.Amount, // a higher fee if not enough was originally chosen. txNoInputs := msgtx.Copy() - unspent, err := a.TxStore.UnspentOutputs() + unspent, err := w.TxStore.UnspentOutputs() if err != nil { return nil, err } @@ -192,7 +192,7 @@ func (a *Account) txToPairs(pairs map[string]btcutil.Amount, } // Locked unspent outputs are skipped. - if a.LockedOutpoint(*unspent[i].OutPoint()) { + if w.LockedOutpoint(*unspent[i].OutPoint()) { continue } @@ -208,12 +208,14 @@ func (a *Account) txToPairs(pairs map[string]btcutil.Amount, // changeAddr is nil/zeroed until a change address is needed, and reused // again in case a change utxo has already been chosen. var changeAddr btcutil.Address + var changeIdx int // Get the number of satoshis to increment fee by when searching for // the minimum tx fee needed. fee := btcutil.Amount(0) for { msgtx = txNoInputs.Copy() + changeIdx = -1 // Select eligible outputs to be used in transaction based on the amount // neededing to sent, and the current fee estimation. @@ -228,13 +230,16 @@ func (a *Account) txToPairs(pairs map[string]btcutil.Amount, if change > 0 { // Get a new change address if one has not already been found. if changeAddr == nil { - changeAddr, err = a.KeyStore.ChangeAddress(&bs, cfg.KeypoolSize) + changeAddr, err = w.KeyStore.ChangeAddress(bs) if err != nil { return nil, fmt.Errorf("failed to get next address: %s", err) } - - // Mark change address as belonging to this account. - AcctMgr.MarkAddressForAccount(changeAddr, a) + w.KeyStore.MarkDirty() + err = w.chainSvr.NotifyReceived([]btcutil.Address{changeAddr}) + if err != nil { + return nil, fmt.Errorf("cannot request updates for "+ + "change address: %v", err) + } } // Spend change. @@ -249,6 +254,7 @@ func (a *Account) txToPairs(pairs map[string]btcutil.Amount, r := rng.Int31n(int32(len(msgtx.TxOut))) // random index c := len(msgtx.TxOut) - 1 // change index msgtx.TxOut[r], msgtx.TxOut[c] = msgtx.TxOut[c], msgtx.TxOut[r] + changeIdx = int(r) } // Selected unspent outputs become new transaction's inputs. @@ -264,10 +270,10 @@ func (a *Account) txToPairs(pairs map[string]btcutil.Amount, } apkh, ok := addrs[0].(*btcutil.AddressPubKeyHash) if !ok { - continue // don't handle inputs to this yes + continue // don't handle inputs to this yet } - ai, err := a.KeyStore.Address(apkh) + ai, err := w.KeyStore.Address(apkh) if err != nil { return nil, fmt.Errorf("cannot get address info: %v", err) } @@ -275,10 +281,8 @@ func (a *Account) txToPairs(pairs map[string]btcutil.Amount, pka := ai.(keystore.PubKeyAddress) privkey, err := pka.PrivKey() - if err == keystore.ErrLocked { - return nil, keystore.ErrLocked - } else if err != nil { - return nil, fmt.Errorf("cannot get address key: %v", err) + if err != nil { + return nil, fmt.Errorf("cannot get private key: %v", err) } sigscript, err := btcscript.SignatureScript(msgtx, i, @@ -294,7 +298,7 @@ func (a *Account) txToPairs(pairs map[string]btcutil.Amount, if !cfg.DisallowFree { noFeeAllowed = allowFree(bs.Height, inputs, msgtx.SerializeSize()) } - if minFee := minimumFee(a.FeeIncrement, msgtx, noFeeAllowed); fee < minFee { + if minFee := minimumFee(w.FeeIncrement, msgtx, noFeeAllowed); fee < minFee { fee = minFee } else { selectedInputs = inputs @@ -327,9 +331,9 @@ func (a *Account) txToPairs(pairs map[string]btcutil.Amount, panic(err) } info := &CreatedTx{ - tx: btcutil.NewTx(msgtx), - inputs: selectedInputs, - changeAddr: changeAddr, + tx: btcutil.NewTx(msgtx), + changeAddr: changeAddr, + changeIndex: changeIdx, } return info, nil } diff --git a/createtx_test.go b/createtx_test.go index 264056a..636ee0e 100644 --- a/createtx_test.go +++ b/createtx_test.go @@ -89,7 +89,7 @@ func TestFakeTxs(t *testing.T) { t.Errorf("Can not create encrypted wallet: %s", err) return } - a := &Account{ + a := &Wallet{ Wallet: w, lockedOutpoints: map[btcwire.OutPoint]struct{}{}, } diff --git a/disksync.go b/disksync.go index 2b434ba..b8d8748 100644 --- a/disksync.go +++ b/disksync.go @@ -18,68 +18,9 @@ package main import ( "fmt" - "io/ioutil" "os" - "path/filepath" - "time" - - "github.com/conformal/btcnet" - "github.com/conformal/btcwire" ) -// networkDir returns the directory name of a network directory to hold account -// files. -func networkDir(net *btcnet.Params) string { - netname := net.Name - - // For now, we must always name the testnet data directory as "testnet" - // and not "testnet3" or any other version, as the btcnet testnet3 - // paramaters will likely be switched to being named "testnet3" in the - // future. This is done to future proof that change, and an upgrade - // plan to move the testnet3 data directory can be worked out later. - if net.Net == btcwire.TestNet3 { - netname = "testnet" - } - - return filepath.Join(cfg.DataDir, netname) -} - -// tmpNetworkDir returns the temporary directory name for a given network. -func tmpNetworkDir(net *btcnet.Params) string { - return networkDir(net) + "_tmp" -} - -// freshDir creates a new directory specified by path if it does not -// exist. If the directory already exists, all files contained in the -// directory are removed. -func freshDir(path string) error { - if err := checkCreateDir(path); err != nil { - return err - } - - // Remove all files in the directory. - fd, err := os.Open(path) - if err != nil { - return err - } - defer func() { - if err := fd.Close(); err != nil { - log.Warnf("Cannot close directory: %v", err) - } - }() - names, err := fd.Readdirnames(0) - if err != nil { - return err - } - for _, name := range names { - if err := os.RemoveAll(name); err != nil { - return err - } - } - - return nil -} - // checkCreateDir checks that the path exists and is a directory. // If path does not exist, it is created. func checkCreateDir(path string) error { @@ -100,340 +41,3 @@ func checkCreateDir(path string) error { return nil } - -// accountFilename returns the filepath of an account file given the -// filename suffix ("wallet.bin", or "tx.bin"), account name and the -// network directory holding the file. -func accountFilename(suffix, account, netdir string) string { - if account == "" { - // default account - return filepath.Join(netdir, suffix) - } - - // non-default account - return filepath.Join(netdir, fmt.Sprintf("%v-%v", account, suffix)) -} - -// syncSchedule references the account files which have been -// scheduled to be written and the directory to write to. -type syncSchedule struct { - dir string - wallets map[*Account]struct{} - txs map[*Account]struct{} -} - -func newSyncSchedule(dir string) *syncSchedule { - s := &syncSchedule{ - dir: dir, - wallets: make(map[*Account]struct{}), - txs: make(map[*Account]struct{}), - } - return s -} - -// flushAccount writes all scheduled account files to disk for -// a single account and removes them from the schedule. -func (s *syncSchedule) flushAccount(a *Account) error { - if _, ok := s.txs[a]; ok { - if err := a.writeTxStore(s.dir); err != nil { - return err - } - delete(s.txs, a) - } - if _, ok := s.wallets[a]; ok { - if err := a.writeWallet(s.dir); err != nil { - return err - } - delete(s.wallets, a) - } - - return nil -} - -// flush writes all scheduled account files and removes each -// from the schedule. -func (s *syncSchedule) flush() error { - for a := range s.txs { - if err := a.writeTxStore(s.dir); err != nil { - return err - } - delete(s.txs, a) - } - - for a := range s.wallets { - if err := a.writeWallet(s.dir); err != nil { - return err - } - delete(s.wallets, a) - } - - return nil -} - -type flushAccountRequest struct { - a *Account - err chan error -} - -type writeBatchRequest struct { - a []*Account - err chan error -} - -type exportRequest struct { - dir string - a *Account - err chan error -} - -// DiskSyncer manages all disk write operations for a collection of accounts. -type DiskSyncer struct { - // Flush scheduled account writes. - flushAccount chan *flushAccountRequest - - // Schedule file writes for an account. - scheduleWallet chan *Account - scheduleTxStore chan *Account - - // Write a collection of accounts all at once. - writeBatch chan *writeBatchRequest - - // Write an account export. - exportAccount chan *exportRequest - - // Account manager for this DiskSyncer. This is only - // needed to grab the account manager semaphore. - am *AccountManager - - quit chan struct{} - shutdown chan struct{} -} - -// NewDiskSyncer creates a new DiskSyncer. -func NewDiskSyncer(am *AccountManager) *DiskSyncer { - return &DiskSyncer{ - flushAccount: make(chan *flushAccountRequest), - scheduleWallet: make(chan *Account), - scheduleTxStore: make(chan *Account), - writeBatch: make(chan *writeBatchRequest), - exportAccount: make(chan *exportRequest), - am: am, - quit: make(chan struct{}), - shutdown: make(chan struct{}), - } -} - -// Start starts the goroutines required to run the DiskSyncer. -func (ds *DiskSyncer) Start() { - go ds.handler() -} - -func (ds *DiskSyncer) Stop() { - close(ds.quit) -} - -func (ds *DiskSyncer) WaitForShutdown() { - <-ds.shutdown -} - -// handler runs the disk syncer. It manages a set of "dirty" account files -// which must be written to disk, and synchronizes all writes in a single -// goroutine. Periodic flush operations may be signaled by an AccountManager. -// -// This never returns and is should be called from a new goroutine. -func (ds *DiskSyncer) handler() { - netdir := networkDir(activeNet.Params) - if err := checkCreateDir(netdir); err != nil { - log.Errorf("Unable to create or write to account directory: %v", err) - } - tmpnetdir := tmpNetworkDir(activeNet.Params) - - const wait = 10 * time.Second - var timer <-chan time.Time - var sem chan struct{} - schedule := newSyncSchedule(netdir) -out: - for { - select { - case <-sem: // Now have exclusive access of the account manager - err := schedule.flush() - if err != nil { - log.Errorf("Cannot write accounts: %v", err) - } - - timer = nil - - // Do not grab semaphore again until another flush is needed. - sem = nil - - // Release semaphore. - ds.am.bsem <- struct{}{} - - case <-timer: - // Grab AccountManager semaphore when ready so flush can occur. - sem = ds.am.bsem - - case fr := <-ds.flushAccount: - fr.err <- schedule.flushAccount(fr.a) - - case a := <-ds.scheduleWallet: - schedule.wallets[a] = struct{}{} - if timer == nil { - timer = time.After(wait) - } - - case a := <-ds.scheduleTxStore: - schedule.txs[a] = struct{}{} - if timer == nil { - timer = time.After(wait) - } - - case sr := <-ds.writeBatch: - err := batchWriteAccounts(sr.a, tmpnetdir, netdir) - if err == nil { - // All accounts have been synced, old schedule - // can be discarded. - schedule = newSyncSchedule(netdir) - timer = nil - } - sr.err <- err - - case er := <-ds.exportAccount: - a := er.a - dir := er.dir - er.err <- a.writeAll(dir) - - case <-ds.quit: - err := schedule.flush() - if err != nil { - log.Errorf("Cannot write accounts: %v", err) - } - break out - } - } - close(ds.shutdown) -} - -// FlushAccount writes all scheduled account files to disk for a single -// account. -func (ds *DiskSyncer) FlushAccount(a *Account) error { - err := make(chan error) - ds.flushAccount <- &flushAccountRequest{a: a, err: err} - return <-err -} - -// ScheduleWalletWrite schedules an account's wallet to be written to disk. -func (ds *DiskSyncer) ScheduleWalletWrite(a *Account) { - ds.scheduleWallet <- a -} - -// ScheduleTxStoreWrite schedules an account's transaction store to be -// written to disk. -func (ds *DiskSyncer) ScheduleTxStoreWrite(a *Account) { - ds.scheduleTxStore <- a -} - -// WriteBatch safely replaces all account files in the network directory -// with new files created from all accounts in a. -func (ds *DiskSyncer) WriteBatch(a []*Account) error { - err := make(chan error) - ds.writeBatch <- &writeBatchRequest{ - a: a, - err: err, - } - return <-err -} - -// ExportAccount writes all account files for a to a new directory. -func (ds *DiskSyncer) ExportAccount(a *Account, dir string) error { - err := make(chan error) - er := &exportRequest{ - dir: dir, - a: a, - err: err, - } - ds.exportAccount <- er - return <-err -} - -func batchWriteAccounts(accts []*Account, tmpdir, netdir string) error { - if err := freshDir(tmpdir); err != nil { - return err - } - for _, a := range accts { - if err := a.writeAll(tmpdir); err != nil { - return err - } - } - // This is technically NOT an atomic operation, but at startup, if the - // network directory is missing but the temporary network directory - // exists, the temporary is moved before accounts are opened. - if err := os.RemoveAll(netdir); err != nil { - return err - } - if err := Rename(tmpdir, netdir); err != nil { - return err - } - return nil -} - -func (a *Account) writeAll(dir string) error { - if err := a.writeTxStore(dir); err != nil { - return err - } - if err := a.writeWallet(dir); err != nil { - return err - } - return nil -} - -func (a *Account) writeWallet(dir string) error { - wfilepath := accountFilename("wallet.bin", a.name, dir) - _, filename := filepath.Split(wfilepath) - tmpfile, err := ioutil.TempFile(dir, filename) - if err != nil { - return err - } - if _, err = a.KeyStore.WriteTo(tmpfile); err != nil { - return err - } - - tmppath := tmpfile.Name() - if err := tmpfile.Sync(); err != nil { - log.Warnf("Failed to sync temporary wallet file %s: %v", - tmppath, err) - } - - if err := tmpfile.Close(); err != nil { - log.Warnf("Cannot close temporary wallet file %s: %v", - tmppath, err) - } - - return Rename(tmppath, wfilepath) -} - -func (a *Account) writeTxStore(dir string) error { - txfilepath := accountFilename("tx.bin", a.name, dir) - _, filename := filepath.Split(txfilepath) - tmpfile, err := ioutil.TempFile(dir, filename) - if err != nil { - return err - } - - if _, err = a.TxStore.WriteTo(tmpfile); err != nil { - return err - } - - tmppath := tmpfile.Name() - if err := tmpfile.Sync(); err != nil { - log.Warnf("Failed to sync temporary txstore file %s: %v", - tmppath, err) - } - - if err := tmpfile.Close(); err != nil { - log.Warnf("Cannot close temporary txstore file %s: %v", - tmppath, err) - } - - return Rename(tmppath, txfilepath) -} diff --git a/keystore/keystore.go b/keystore/keystore.go index 23a1560..e76136b 100644 --- a/keystore/keystore.go +++ b/keystore/keystore.go @@ -22,14 +22,17 @@ import ( "crypto/cipher" "crypto/ecdsa" "crypto/rand" - "crypto/sha256" "crypto/sha512" "encoding/binary" "encoding/hex" "errors" "fmt" "io" + "io/ioutil" "math/big" + "os" + "path/filepath" + "sync" "time" "code.google.com/p/go.crypto/ripemd160" @@ -38,10 +41,13 @@ import ( "github.com/conformal/btcnet" "github.com/conformal/btcscript" "github.com/conformal/btcutil" + "github.com/conformal/btcwallet/rename" "github.com/conformal/btcwire" ) const ( + filename = "wallet.bin" + // Length in bytes of KDF output. kdfOutputBytes = 32 @@ -67,8 +73,7 @@ var ( ErrWrongPassphrase = errors.New("wrong passphrase") ) -// '\xbaWALLET\x00' -var fileID = [8]byte{0xba, 0x57, 0x41, 0x4c, 0x4c, 0x45, 0x54, 0x00} +var fileID = [8]byte{0xba, 'W', 'A', 'L', 'L', 'E', 'T', 0x00} type entryHeader byte @@ -155,11 +160,11 @@ func keyOneIter(passphrase, salt []byte, memReqts uint64) []byte { return x[:kdfOutputBytes] } -// Key implements the key derivation function used by Armory +// kdf implements the key derivation function used by Armory // based on the ROMix algorithm described in Colin Percival's paper // "Stronger Key Derivation via Sequential Memory-Hard Functions" // (http://www.tarsnap.com/scrypt/scrypt.pdf). -func Key(passphrase []byte, params *kdfParameters) []byte { +func kdf(passphrase []byte, params *kdfParameters) []byte { masterKey := passphrase for i := uint32(0); i < params.nIter; i++ { masterKey = keyOneIter(masterKey, params.salt[:], params.mem) @@ -178,10 +183,10 @@ func pad(size int, b []byte) []byte { return p } -// ChainedPrivKey deterministically generates a new private key using a +// chainedPrivKey deterministically generates a new private key using a // previous address and chaincode. privkey and chaincode must be 32 // bytes long, and pubkey may either be 33 or 65 bytes. -func ChainedPrivKey(privkey, pubkey, chaincode []byte) ([]byte, error) { +func chainedPrivKey(privkey, pubkey, chaincode []byte) ([]byte, error) { if len(privkey) != 32 { return nil, fmt.Errorf("invalid privkey length %d (must be 32)", len(privkey)) @@ -210,10 +215,10 @@ func ChainedPrivKey(privkey, pubkey, chaincode []byte) ([]byte, error) { return pad(32, b), nil } -// ChainedPubKey deterministically generates a new public key using a +// chainedPubKey deterministically generates a new public key using a // previous public key and chaincode. pubkey must be 33 or 65 bytes, and // chaincode must be 32 bytes long. -func ChainedPubKey(pubkey, chaincode []byte) ([]byte, error) { +func chainedPubKey(pubkey, chaincode []byte) ([]byte, error) { var compressed bool switch n := len(pubkey); n { case btcec.PubKeyBytesLenUncompressed: @@ -267,11 +272,11 @@ type version struct { var _ io.ReaderFrom = &version{} var _ io.WriterTo = &version{} -// ReaderFromVersion is an io.ReaderFrom and io.WriterTo that +// readerFromVersion is an io.ReaderFrom and io.WriterTo that // can specify any particular key store file format for reading // depending on the key store file version. -type ReaderFromVersion interface { - ReadFromVersion(version, io.Reader) (int64, error) +type readerFromVersion interface { + readFromVersion(version, io.Reader) (int64, error) io.WriterTo } @@ -455,26 +460,6 @@ func (v *varEntries) ReadFrom(r io.Reader) (n int64, err error) { } n += read wt = &entry - case addrCommentHeader: - var entry addrCommentEntry - if read, err = entry.ReadFrom(r); err != nil { - return n + read, err - } - n += read - wt = &entry - case txCommentHeader: - var entry txCommentEntry - if read, err = entry.ReadFrom(r); err != nil { - return n + read, err - } - n += read - wt = &entry - case deletedHeader: - var entry deletedEntry - if read, err = entry.ReadFrom(r); err != nil { - return n + read, err - } - n += read default: return n, fmt.Errorf("unknown entry header: %d", uint8(header)) } @@ -505,11 +490,11 @@ func (net *netParams) ReadFrom(r io.Reader) (int64, error) { switch btcwire.BitcoinNet(binary.LittleEndian.Uint32(uint32Bytes)) { case btcwire.MainNet: - *net = *(*netParams)(&btcnet.MainNetParams) + *net = (netParams)(btcnet.MainNetParams) case btcwire.TestNet3: - *net = *(*netParams)(&btcnet.TestNet3Params) + *net = (netParams)(btcnet.TestNet3Params) case btcwire.SimNet: - *net = *(*netParams)(&btcnet.SimNetParams) + *net = (netParams)(btcnet.SimNetParams) default: return n64, errors.New("unknown network") } @@ -540,6 +525,14 @@ func getAddressKey(addr btcutil.Address) addressKey { // io.ReaderFrom and io.WriterTo interfaces to read from and // write to any type of byte streams, including files. type Store struct { + // TODO: Use atomic operations for dirty so the reader lock + // doesn't need to be grabbed. + dirty bool + path string + dir string + file string + + mtx sync.RWMutex vers version net *netParams flags walletFlags @@ -554,9 +547,7 @@ type Store struct { // root address and the appended entries. recent recentBlocks - addrMap map[addressKey]walletAddress - addrCommentMap map[addressKey]comment - txCommentMap map[transactionHashKey]comment + addrMap map[addressKey]walletAddress // The rest of the fields in this struct are not serialized. passphrase []byte @@ -567,26 +558,23 @@ type Store struct { missingKeysStart int64 } -// NewStore creates and initializes a new Store. name's and -// desc's binary representation must not exceed 32 and 256 bytes, -// respectively. All address private keys are encrypted with passphrase. -// The key store is returned locked. -func NewStore(name, desc string, passphrase []byte, net *btcnet.Params, - createdAt *BlockStamp, keypoolSize uint) (*Store, error) { +// New creates and initializes a new Store. name's and desc's byte length +// must not exceed 32 and 256 bytes, respectively. All address private keys +// are encrypted with passphrase. The key store is returned locked. +func New(dir string, desc string, passphrase []byte, net *btcnet.Params, + createdAt *BlockStamp) (*Store, error) { // Check sizes of inputs. - if len([]byte(name)) > 32 { - return nil, errors.New("name exceeds 32 byte maximum size") - } - if len([]byte(desc)) > 256 { + if len(desc) > 256 { return nil, errors.New("desc exceeds 256 byte maximum size") } // Randomly-generate rootkey and chaincode. - rootkey, chaincode := make([]byte, 32), make([]byte, 32) + rootkey := make([]byte, 32) if _, err := rand.Read(rootkey); err != nil { return nil, err } + chaincode := make([]byte, 32) if _, err := rand.Read(chaincode); err != nil { return nil, err } @@ -596,10 +584,13 @@ func NewStore(name, desc string, passphrase []byte, net *btcnet.Params, if err != nil { return nil, err } - aeskey := Key([]byte(passphrase), kdfp) + aeskey := kdf(passphrase, kdfp) // Create and fill key store. s := &Store{ + path: filepath.Join(dir, filename), + dir: dir, + file: filename, vers: VersCurrent, net: (*netParams)(net), flags: walletFlags{ @@ -612,17 +603,15 @@ func NewStore(name, desc string, passphrase []byte, net *btcnet.Params, recent: recentBlocks{ lastHeight: createdAt.Height, hashes: []*btcwire.ShaHash{ - &createdAt.Hash, + createdAt.Hash, }, }, - addrMap: make(map[addressKey]walletAddress), - addrCommentMap: make(map[addressKey]comment), - txCommentMap: make(map[transactionHashKey]comment), - chainIdxMap: make(map[int64]btcutil.Address), - lastChainIdx: rootKeyChainIdx, - secret: aeskey, + addrMap: make(map[addressKey]walletAddress), + chainIdxMap: make(map[int64]btcutil.Address), + lastChainIdx: rootKeyChainIdx, + missingKeysStart: rootKeyChainIdx, + secret: aeskey, } - copy(s.name[:], []byte(name)) copy(s.desc[:], []byte(desc)) // Create new root address from key and chaincode. @@ -648,11 +637,6 @@ func NewStore(name, desc string, passphrase []byte, net *btcnet.Params, s.addrMap[getAddressKey(rootAddr)] = &s.keyGenerator s.chainIdxMap[rootKeyChainIdx] = rootAddr - // Fill keypool. - if err := s.extendKeypool(keypoolSize, createdAt); err != nil { - return nil, err - } - // key store must be returned locked. if err := s.Lock(); err != nil { return nil, err @@ -661,29 +645,17 @@ func NewStore(name, desc string, passphrase []byte, net *btcnet.Params, return s, nil } -// Name returns the name of a key store. This name is used as the -// account name for btcwallet JSON methods. -func (s *Store) Name() string { - last := len(s.name[:]) - for i, b := range s.name[:] { - if b == 0x00 { - last = i - break - } - } - return string(s.name[:last]) -} - // ReadFrom reads data from a io.Reader and saves it to a key store, // returning the number of bytes read and any errors encountered. func (s *Store) ReadFrom(r io.Reader) (n int64, err error) { + s.mtx.Lock() + defer s.mtx.Unlock() + var read int64 s.net = &netParams{} s.addrMap = make(map[addressKey]walletAddress) - s.addrCommentMap = make(map[addressKey]comment) s.chainIdxMap = make(map[int64]btcutil.Address) - s.txCommentMap = make(map[transactionHashKey]comment) var id [8]byte appendedEntries := varEntries{store: s} @@ -711,8 +683,8 @@ func (s *Store) ReadFrom(r io.Reader) (n int64, err error) { for _, data := range datas { var err error switch d := data.(type) { - case ReaderFromVersion: - read, err = d.ReadFromVersion(s.vers, r) + case readerFromVersion: + read, err = d.readFromVersion(s.vers, r) case io.ReaderFrom: read, err = d.ReadFrom(r) @@ -755,7 +727,7 @@ func (s *Store) ReadFrom(r io.Reader) (n int64, err error) { // earliest so all can be created on next key store unlock. if e.addr.flags.createPrivKeyNextUnlock { switch { - case s.missingKeysStart == 0: + case s.missingKeysStart == rootKeyChainIdx: fallthrough case e.addr.chainIndex < s.missingKeysStart: s.missingKeysStart = e.addr.chainIndex @@ -768,18 +740,6 @@ func (s *Store) ReadFrom(r io.Reader) (n int64, err error) { // script are always imported. s.importedAddrs = append(s.importedAddrs, &e.script) - case *addrCommentEntry: - addr, err := e.address(s.Net()) - if err != nil { - return 0, err - } - s.addrCommentMap[getAddressKey(addr)] = - comment(e.comment) - - case *txCommentEntry: - txKey := transactionHashKey(e.txHash[:]) - s.txCommentMap[txKey] = comment(e.comment) - default: return n, errors.New("unknown appended entry") } @@ -791,6 +751,13 @@ func (s *Store) ReadFrom(r io.Reader) (n int64, err error) { // WriteTo serializes a key store and writes it to a io.Writer, // returning the number of bytes written and any errors encountered. func (s *Store) WriteTo(w io.Writer) (n int64, err error) { + s.mtx.RLock() + defer s.mtx.RUnlock() + + return s.writeTo(w) +} + +func (s *Store) writeTo(w io.Writer) (n int64, err error) { var wts []io.WriterTo var chainedAddrs = make([]io.WriterTo, len(s.chainIdxMap)-1) var importedAddrs []io.WriterTo @@ -820,22 +787,6 @@ func (s *Store) WriteTo(w io.Writer) (n int64, err error) { } } wts = append(chainedAddrs, importedAddrs...) - for addr, comment := range s.addrCommentMap { - e := &addrCommentEntry{ - comment: []byte(comment), - } - // addresskey is the pubkey hash as a string, we can cast it - // safely (though a little distasteful). - copy(e.pubKeyHash160[:], []byte(addr)) - wts = append(wts, e) - } - for hash, comment := range s.txCommentMap { - e := &txCommentEntry{ - comment: []byte(comment), - } - copy(e.txHash[:], []byte(hash)) - wts = append(wts, e) - } appendedEntries := varEntries{store: s, entries: wts} // Iterate through each entry needing to be written. If data @@ -873,6 +824,77 @@ func (s *Store) WriteTo(w io.Writer) (n int64, err error) { return n, nil } +// TODO: set this automatically. +func (s *Store) MarkDirty() { + s.mtx.Lock() + defer s.mtx.Unlock() + + s.dirty = true +} + +func (s *Store) WriteIfDirty() error { + s.mtx.RLock() + if !s.dirty { + s.mtx.RUnlock() + return nil + } + + // TempFile creates the file 0600, so no need to chmod it. + fi, err := ioutil.TempFile(s.dir, s.file) + if err != nil { + s.mtx.RUnlock() + return err + } + fiPath := fi.Name() + + _, err = s.writeTo(fi) + if err != nil { + s.mtx.RUnlock() + fi.Close() + return err + } + err = fi.Sync() + if err != nil { + s.mtx.RUnlock() + fi.Close() + return err + } + fi.Close() + + err = rename.Atomic(fiPath, s.path) + s.mtx.RUnlock() + + if err == nil { + s.mtx.Lock() + s.dirty = false + s.mtx.Unlock() + } + + return err +} + +// OpenDir opens a new key store from the specified directory. If the file +// does not exist, the error from the os package will be returned, and can +// be checked with os.IsNotExist to differentiate missing file errors from +// others (including deserialization). +func OpenDir(dir string) (*Store, error) { + path := filepath.Join(dir, filename) + fi, err := os.OpenFile(path, os.O_RDONLY, 0) + if err != nil { + return nil, err + } + defer fi.Close() + store := new(Store) + _, err = store.ReadFrom(fi) + if err != nil { + return nil, err + } + store.path = path + store.dir = dir + store.file = filename + return store, nil +} + // Unlock derives an AES key from passphrase and key store's KDF // parameters and unlocks the root key of the key store. If // the unlock was successful, the key store's secret key is saved, @@ -880,12 +902,15 @@ func (s *Store) WriteTo(w io.Writer) (n int64, err error) { // addresses created while the key store was locked without private // keys are created at this time. func (s *Store) Unlock(passphrase []byte) error { + s.mtx.Lock() + defer s.mtx.Unlock() + if s.flags.watchingOnly { return ErrWatchingOnly } // Derive key from KDF parameters and passphrase. - key := Key(passphrase, &s.kdfParams) + key := kdf(passphrase, &s.kdfParams) // Unlock root address with derived key. if _, err := s.keyGenerator.unlock(key); err != nil { @@ -902,12 +927,15 @@ func (s *Store) Unlock(passphrase []byte) error { // Lock performs a best try effort to remove and zero all secret keys // associated with the key store. func (s *Store) Lock() (err error) { + s.mtx.Lock() + defer s.mtx.Unlock() + if s.flags.watchingOnly { return ErrWatchingOnly } // Remove clear text passphrase from key store. - if s.IsLocked() { + if s.isLocked() { err = ErrLocked } else { zero(s.passphrase) @@ -926,33 +954,22 @@ func (s *Store) Lock() (err error) { return err } -// Passphrase returns the passphrase for an unlocked key store, or -// ErrWalletLocked if the key store is locked. This should only -// be used for creating key stores for new accounts with the same -// passphrase as other btcwallet account key stores. -// -// The returned byte slice points to internal key store memory and -// will be zeroed when the key store is locked. -func (s *Store) Passphrase() ([]byte, error) { - if len(s.passphrase) != 0 { - return s.passphrase, nil - } - return nil, ErrLocked -} - // ChangePassphrase creates a new AES key from a new passphrase and // re-encrypts all encrypted private keys with the new key. func (s *Store) ChangePassphrase(new []byte) error { + s.mtx.Lock() + defer s.mtx.Unlock() + if s.flags.watchingOnly { return ErrWatchingOnly } - if s.IsLocked() { + if s.isLocked() { return ErrLocked } oldkey := s.secret - newkey := Key(new, &s.kdfParams) + newkey := kdf(new, &s.kdfParams) for _, wa := range s.addrMap { // Only btcAddresses curently have private keys. @@ -986,28 +1003,42 @@ func zero(b []byte) { // IsLocked returns whether a key store is unlocked (in which case the // key is saved in memory), or locked. func (s *Store) IsLocked() bool { + s.mtx.RLock() + defer s.mtx.RUnlock() + + return s.isLocked() +} + +func (s *Store) isLocked() bool { return len(s.secret) != 32 } -// NextChainedAddress attempts to get the next chained address. -// If there are addresses available in the keypool, the next address -// is used. If not and the key store is unlocked, the keypool is extended. -// If locked, a new address's pubkey is chained off the last pubkey -// and added to the key store. -func (s *Store) NextChainedAddress(bs *BlockStamp, keypoolSize uint) (btcutil.Address, error) { - addr, err := s.nextChainedAddress(bs, keypoolSize) +// NextChainedAddress attempts to get the next chained address. If the key +// store is unlocked, the next pubkey and private key of the address chain are +// derived. If the key store is locke, only the next pubkey is derived, and +// the private key will be generated on next unlock. +func (s *Store) NextChainedAddress(bs *BlockStamp) (btcutil.Address, error) { + s.mtx.Lock() + defer s.mtx.Unlock() + + return s.nextChainedAddress(bs) +} + +func (s *Store) nextChainedAddress(bs *BlockStamp) (btcutil.Address, error) { + addr, err := s.nextChainedBtcAddress(bs) if err != nil { return nil, err } - - // Create and return payment address for address hash. return addr.Address(), nil } // ChangeAddress returns the next chained address from the key store, marking // the address for a change transaction output. -func (s *Store) ChangeAddress(bs *BlockStamp, keypoolSize uint) (btcutil.Address, error) { - addr, err := s.nextChainedAddress(bs, keypoolSize) +func (s *Store) ChangeAddress(bs *BlockStamp) (btcutil.Address, error) { + s.mtx.Lock() + defer s.mtx.Unlock() + + addr, err := s.nextChainedBtcAddress(bs) if err != nil { return nil, err } @@ -1018,18 +1049,18 @@ func (s *Store) ChangeAddress(bs *BlockStamp, keypoolSize uint) (btcutil.Address return addr.Address(), nil } -func (s *Store) nextChainedAddress(bs *BlockStamp, keypoolSize uint) (*btcAddress, error) { +func (s *Store) nextChainedBtcAddress(bs *BlockStamp) (*btcAddress, error) { // Attempt to get address hash of next chained address. nextAPKH, ok := s.chainIdxMap[s.highestUsed+1] if !ok { - // Extending the keypool requires an unlocked key store. - if s.IsLocked() { - if err := s.extendLockedWallet(bs); err != nil { + if s.isLocked() { + // Chain pubkeys. + if err := s.extendLocked(bs); err != nil { return nil, err } } else { - // Key is available, extend keypool. - if err := s.extendKeypool(keypoolSize, bs); err != nil { + // Chain private and pubkeys. + if err := s.extendUnlocked(bs); err != nil { return nil, err } } @@ -1061,11 +1092,14 @@ func (s *Store) nextChainedAddress(bs *BlockStamp, keypoolSize uint) (*btcAddres // address from calling NextChainedAddress, or the root address if // no chained addresses have been requested. func (s *Store) LastChainedAddress() btcutil.Address { + s.mtx.RLock() + defer s.mtx.RUnlock() + return s.chainIdxMap[s.highestUsed] } -// extendKeypool grows the keypool by n addresses. -func (s *Store) extendKeypool(n uint, bs *BlockStamp) error { +// extendUnlocked grows address chain for an unlocked keystore. +func (s *Store) extendUnlocked(bs *BlockStamp) error { // Get last chained address. New chained addresses will be // chained off of this address's chaincode and private key. a := s.chainIdxMap[s.lastChainIdx] @@ -1074,59 +1108,50 @@ func (s *Store) extendKeypool(n uint, bs *BlockStamp) error { return errors.New("expected last chained address not found") } - if s.IsLocked() { + if s.isLocked() { return ErrLocked } - addr, ok := waddr.(*btcAddress) + lastAddr, ok := waddr.(*btcAddress) if !ok { return errors.New("found non-pubkey chained address") } - privkey, err := addr.unlock(s.secret) + privkey, err := lastAddr.unlock(s.secret) if err != nil { return err } - cc := addr.chaincode[:] + cc := lastAddr.chaincode[:] - // Create n encrypted addresses and add each to the key store's - // bookkeeping maps. - for i := uint(0); i < n; i++ { - privkey, err = ChainedPrivKey(privkey, addr.pubKeyBytes(), cc) - if err != nil { - return err - } - newaddr, err := newBtcAddress(s, privkey, nil, bs, true) - if err != nil { - return err - } - if err := newaddr.verifyKeypairs(); err != nil { - return err - } - if err = newaddr.encrypt(s.secret); err != nil { - return err - } - a := newaddr.Address() - s.addrMap[getAddressKey(a)] = newaddr - newaddr.chainIndex = addr.chainIndex + 1 - s.chainIdxMap[newaddr.chainIndex] = a - s.lastChainIdx++ - - // armory does this.. but all the chaincodes are equal so why - // not use the root's? - copy(newaddr.chaincode[:], cc) - addr = newaddr + privkey, err = chainedPrivKey(privkey, lastAddr.pubKeyBytes(), cc) + if err != nil { + return err } + newAddr, err := newBtcAddress(s, privkey, nil, bs, true) + if err != nil { + return err + } + if err := newAddr.verifyKeypairs(); err != nil { + return err + } + if err = newAddr.encrypt(s.secret); err != nil { + return err + } + a = newAddr.Address() + s.addrMap[getAddressKey(a)] = newAddr + newAddr.chainIndex = lastAddr.chainIndex + 1 + s.chainIdxMap[newAddr.chainIndex] = a + s.lastChainIdx++ + copy(newAddr.chaincode[:], cc) return nil } -// extendLockedWallet creates one new address without a private key -// (allowing for extending the address chain from a locked key store) -// chained from the last used chained address and adds the address to -// the key store's internal bookkeeping structures. This function should -// not be called unless the keypool has been depleted. -func (s *Store) extendLockedWallet(bs *BlockStamp) error { +// extendLocked creates one new address without a private key (allowing for +// extending the address chain from a locked key store) chained from the +// last used chained address and adds the address to the key store's internal +// bookkeeping structures. +func (s *Store) extendLocked(bs *BlockStamp) error { a := s.chainIdxMap[s.lastChainIdx] waddr, ok := s.addrMap[getAddressKey(a)] if !ok { @@ -1140,7 +1165,7 @@ func (s *Store) extendLockedWallet(bs *BlockStamp) error { cc := addr.chaincode[:] - nextPubkey, err := ChainedPubKey(addr.pubKeyBytes(), cc) + nextPubkey, err := chainedPubKey(addr.pubKeyBytes(), cc) if err != nil { return err } @@ -1155,7 +1180,7 @@ func (s *Store) extendLockedWallet(bs *BlockStamp) error { s.lastChainIdx++ copy(newaddr.chaincode[:], cc) - if s.missingKeysStart == 0 { + if s.missingKeysStart == rootKeyChainIdx { s.missingKeysStart = newaddr.chainIndex } @@ -1164,7 +1189,7 @@ func (s *Store) extendLockedWallet(bs *BlockStamp) error { func (s *Store) createMissingPrivateKeys() error { idx := s.missingKeysStart - if idx == 0 { + if idx == rootKeyChainIdx { return nil } @@ -1174,7 +1199,7 @@ func (s *Store) createMissingPrivateKeys() error { return errors.New("missing previous chained address") } prevWAddr := s.addrMap[getAddressKey(apkh)] - if s.IsLocked() { + if s.isLocked() { return ErrLocked } @@ -1190,7 +1215,7 @@ func (s *Store) createMissingPrivateKeys() error { for i := idx; ; i++ { // Get the next private key for the ith address in the address chain. - ithPrivKey, err := ChainedPrivKey(prevPrivKey, + ithPrivKey, err := chainedPrivKey(prevPrivKey, prevAddr.pubKeyBytes(), prevAddr.chaincode[:]) if err != nil { return err @@ -1211,7 +1236,7 @@ func (s *Store) createMissingPrivateKeys() error { addr.privKeyCT = ithPrivKey if err := addr.encrypt(s.secret); err != nil { // Avoid bug: see comment for VersUnsetNeedsPrivkeyFlag. - if err != ErrAlreadyEncrypted || !s.vers.LT(VersUnsetNeedsPrivkeyFlag) { + if err != ErrAlreadyEncrypted || s.vers.LT(VersUnsetNeedsPrivkeyFlag) { return err } } @@ -1222,7 +1247,7 @@ func (s *Store) createMissingPrivateKeys() error { prevPrivKey = ithPrivKey } - s.missingKeysStart = 0 + s.missingKeysStart = rootKeyChainIdx return nil } @@ -1230,6 +1255,9 @@ func (s *Store) createMissingPrivateKeys() error { // This address may be typecast into other interfaces (like PubKeyAddress // and ScriptAddress) if specific information e.g. keys is required. func (s *Store) Address(a btcutil.Address) (WalletAddress, error) { + s.mtx.RLock() + defer s.mtx.RUnlock() + // Look up address by address hash. btcaddr, ok := s.addrMap[getAddressKey(a)] if !ok { @@ -1241,6 +1269,13 @@ func (s *Store) Address(a btcutil.Address) (WalletAddress, error) { // Net returns the bitcoin network parameters for this key store. func (s *Store) Net() *btcnet.Params { + s.mtx.RLock() + defer s.mtx.RUnlock() + + return s.netParams() +} + +func (s *Store) netParams() *btcnet.Params { return (*btcnet.Params)(s.net) } @@ -1250,6 +1285,9 @@ func (s *Store) Net() *btcnet.Params { // When marking an address as unsynced, only the type Unsynced matters. // The value is ignored. func (s *Store) SetSyncStatus(a btcutil.Address, ss SyncStatus) error { + s.mtx.Lock() + defer s.mtx.Unlock() + wa, ok := s.addrMap[getAddressKey(a)] if !ok { return ErrAddressNotFound @@ -1266,6 +1304,9 @@ func (s *Store) SetSyncStatus(a btcutil.Address, ss SyncStatus) error { // // If bs is nil, the entire key store is marked unsynced. func (s *Store) SetSyncedWith(bs *BlockStamp) { + s.mtx.Lock() + defer s.mtx.Unlock() + if bs == nil { s.recent.hashes = s.recent.hashes[:0] s.recent.lastHeight = s.keyGenerator.firstBlock @@ -1279,7 +1320,7 @@ func (s *Store) SetSyncedWith(bs *BlockStamp) { if bs.Height < s.recent.lastHeight { maybeIdx := len(s.recent.hashes) - 1 - int(s.recent.lastHeight-bs.Height) if maybeIdx >= 0 && maybeIdx < len(s.recent.hashes) && - *s.recent.hashes[maybeIdx] == bs.Hash { + *s.recent.hashes[maybeIdx] == *bs.Hash { s.recent.lastHeight = bs.Height // subslice out the removed hashes. @@ -1295,30 +1336,36 @@ func (s *Store) SetSyncedWith(bs *BlockStamp) { s.recent.lastHeight = bs.Height - blockSha := bs.Hash if len(s.recent.hashes) == 20 { // Make room for the most recent hash. copy(s.recent.hashes, s.recent.hashes[1:]) // Set new block in the last position. - s.recent.hashes[19] = &blockSha + s.recent.hashes[19] = bs.Hash } else { - s.recent.hashes = append(s.recent.hashes, &blockSha) + s.recent.hashes = append(s.recent.hashes, bs.Hash) } } -// SyncHeight returns the sync height of a key store, or the earliest -// block height of any unsynced imported address if there are any -// addresses marked as unsynced, whichever is smaller. This is the -// height that rescans on an entire key store should begin at to fully -// sync all key store addresses. -func (s *Store) SyncHeight() int32 { - var height int32 +// SyncHeight returns details about the block that a wallet is marked at least +// synced through. The height is the height that rescans should start at when +// syncing a wallet back to the best chain. +// +// NOTE: If the hash of the synced block is not known, hash will be nil, and +// must be obtained from elsewhere. This must be explicitly checked before +// dereferencing the pointer. +func (s *Store) SyncedTo() (hash *btcwire.ShaHash, height int32) { + s.mtx.RLock() + defer s.mtx.RUnlock() + switch h, ok := s.keyGenerator.SyncStatus().(PartialSync); { case ok && int32(h) > s.recent.lastHeight: height = int32(h) default: height = s.recent.lastHeight + if n := len(s.recent.hashes); n != 0 { + hash = s.recent.hashes[n-1] + } } for _, a := range s.addrMap { var syncHeight int32 @@ -1332,27 +1379,34 @@ func (s *Store) SyncHeight() int32 { } if syncHeight < height { height = syncHeight + hash = nil // Can't go lower than 0. if height == 0 { - break + return } } } - return height + return } // NewIterateRecentBlocks returns an iterator for recently-seen blocks. // The iterator starts at the most recently-added block, and Prev should // be used to access earlier blocks. -func (s *Store) NewIterateRecentBlocks() RecentBlockIterator { - return s.recent.NewIterator() +func (s *Store) NewIterateRecentBlocks() *BlockIterator { + s.mtx.RLock() + defer s.mtx.RUnlock() + + return s.recent.iter(s) } // ImportPrivateKey imports a WIF private key into the keystore. The imported // address is created using either a compressed or uncompressed serialized // public key, depending on the CompressPubKey bool of the WIF. func (s *Store) ImportPrivateKey(wif *btcutil.WIF, bs *BlockStamp) (btcutil.Address, error) { + s.mtx.Lock() + defer s.mtx.Unlock() + if s.flags.watchingOnly { return nil, ErrWatchingOnly } @@ -1365,7 +1419,7 @@ func (s *Store) ImportPrivateKey(wif *btcutil.WIF, bs *BlockStamp) (btcutil.Addr } // The key store must be unlocked to encrypt the imported private key. - if s.IsLocked() { + if s.isLocked() { return nil, ErrLocked } @@ -1402,6 +1456,9 @@ func (s *Store) ImportPrivateKey(wif *btcutil.WIF, bs *BlockStamp) (btcutil.Addr // ImportScript creates a new scriptAddress with a user-provided script // and adds it to the key store. func (s *Store) ImportScript(script []byte, bs *BlockStamp) (btcutil.Address, error) { + s.mtx.Lock() + defer s.mtx.Unlock() + if s.flags.watchingOnly { return nil, ErrWatchingOnly } @@ -1437,6 +1494,9 @@ func (s *Store) ImportScript(script []byte, bs *BlockStamp) (btcutil.Address, er // is used to compare the key store creation time against block headers and // set a better minimum block height of where to being rescans. func (s *Store) CreateDate() int64 { + s.mtx.RLock() + defer s.mtx.RUnlock() + return s.createDate } @@ -1446,6 +1506,9 @@ func (s *Store) CreateDate() int64 { // created the original key store (thanks to public key address chaining), but // will be missing the associated private keys. func (s *Store) ExportWatchingWallet() (*Store, error) { + s.mtx.RLock() + defer s.mtx.RUnlock() + // Don't continue if key store is already watching-only. if s.flags.watchingOnly { return nil, ErrWatchingOnly @@ -1468,9 +1531,7 @@ func (s *Store) ExportWatchingWallet() (*Store, error) { lastHeight: s.recent.lastHeight, }, - addrMap: make(map[addressKey]walletAddress), - addrCommentMap: make(map[addressKey]comment), - txCommentMap: make(map[transactionHashKey]comment), + addrMap: make(map[addressKey]walletAddress), // todo oga make me a list chainIdxMap: make(map[int64]btcutil.Address), @@ -1497,11 +1558,6 @@ func (s *Store) ExportWatchingWallet() (*Store, error) { apkhCopy := apkh ws.addrMap[apkhCopy] = addr.watchingCopy(ws) } - for apkh, cmt := range s.addrCommentMap { - cmtCopy := make(comment, len(cmt)) - copy(cmtCopy, cmt) - ws.addrCommentMap[apkh] = cmtCopy - } if len(s.importedAddrs) != 0 { ws.importedAddrs = make([]walletAddress, 0, len(s.importedAddrs)) @@ -1518,25 +1574,27 @@ type SyncStatus interface { ImplementsSyncStatus() } -// Unsynced is a type representing an unsynced address. When this is -// returned by a key store method, the value is the recorded first seen -// block height. -type Unsynced int32 +type ( + // Unsynced is a type representing an unsynced address. When this is + // returned by a key store method, the value is the recorded first seen + // block height. + Unsynced int32 + + // PartialSync is a type representing a partially synced address (for + // example, due to the result of a partially-completed rescan). + PartialSync int32 + + // FullSync is a type representing an address that is in sync with the + // recently seen blocks. + FullSync struct{} +) // ImplementsSyncStatus is implemented to make Unsynced a SyncStatus. func (u Unsynced) ImplementsSyncStatus() {} -// PartialSync is a type representing a partially synced address (for -// example, due to the result of a partially-completed rescan). -type PartialSync int32 - // ImplementsSyncStatus is implemented to make PartialSync a SyncStatus. func (p PartialSync) ImplementsSyncStatus() {} -// FullSync is a type representing an address that is in sync with the -// recently seen blocks. -type FullSync struct{} - // ImplementsSyncStatus is implemented to make FullSync a SyncStatus. func (f FullSync) ImplementsSyncStatus() {} @@ -1568,6 +1626,9 @@ type WalletAddress interface { // the key pool. Use this when ordered addresses are needed. Otherwise, // ActiveAddresses is preferred. func (s *Store) SortedActiveAddresses() []WalletAddress { + s.mtx.RLock() + defer s.mtx.RUnlock() + addrs := make([]WalletAddress, 0, s.highestUsed+int64(len(s.importedAddrs))+1) for i := int64(rootKeyChainIdx); i <= s.highestUsed; i++ { @@ -1587,6 +1648,9 @@ func (s *Store) SortedActiveAddresses() []WalletAddress { // and their full info. These do not include unused addresses in the // key pool. If addresses must be sorted, use SortedActiveAddresses. func (s *Store) ActiveAddresses() map[btcutil.Address]WalletAddress { + s.mtx.RLock() + defer s.mtx.RUnlock() + addrs := make(map[btcutil.Address]WalletAddress) for i := int64(rootKeyChainIdx); i <= s.highestUsed; i++ { a := s.chainIdxMap[i] @@ -1607,21 +1671,20 @@ func (s *Store) ActiveAddresses() map[btcutil.Address]WalletAddress { // // A slice is returned with the btcutil.Address of each new address. // The blockchain must be rescanned for these addresses. -func (s *Store) ExtendActiveAddresses(n int, keypoolSize uint) ([]btcutil.Address, error) { - if n <= 0 { - return nil, errors.New("n is not positive") - } +func (s *Store) ExtendActiveAddresses(n int) ([]btcutil.Address, error) { + s.mtx.Lock() + defer s.mtx.Unlock() last := s.addrMap[getAddressKey(s.chainIdxMap[s.highestUsed])] bs := &BlockStamp{Height: last.FirstBlock()} - addrs := make([]btcutil.Address, 0, n) + addrs := make([]btcutil.Address, n) for i := 0; i < n; i++ { - addr, err := s.NextChainedAddress(bs, keypoolSize) + addr, err := s.nextChainedAddress(bs) if err != nil { return nil, err } - addrs = append(addrs, addr) + addrs[i] = addr } return addrs, nil } @@ -1737,13 +1800,7 @@ type recentBlocks struct { lastHeight int32 } -type blockIterator struct { - height int32 - index int - rb *recentBlocks -} - -func (rb *recentBlocks) ReadFromVersion(v version, r io.Reader) (int64, error) { +func (rb *recentBlocks) readFromVersion(v version, r io.Reader) (int64, error) { if !v.LT(Vers20LastBlocks) { // Use current version. return rb.ReadFrom(r) @@ -1876,26 +1933,31 @@ func (rb *recentBlocks) WriteTo(w io.Writer) (int64, error) { return written, nil } -// RecentBlockIterator is a type to iterate through recent-seen -// blocks. -type RecentBlockIterator interface { - Next() bool - Prev() bool - BlockStamp() *BlockStamp +// BlockIterator allows for the forwards and backwards iteration of recently +// seen blocks. +type BlockIterator struct { + storeMtx *sync.RWMutex + height int32 + index int + rb *recentBlocks } -func (rb *recentBlocks) NewIterator() RecentBlockIterator { +func (rb *recentBlocks) iter(s *Store) *BlockIterator { if rb.lastHeight == -1 || len(rb.hashes) == 0 { return nil } - return &blockIterator{ - height: rb.lastHeight, - index: len(rb.hashes) - 1, - rb: rb, + return &BlockIterator{ + storeMtx: &s.mtx, + height: rb.lastHeight, + index: len(rb.hashes) - 1, + rb: rb, } } -func (it *blockIterator) Next() bool { +func (it *BlockIterator) Next() bool { + it.storeMtx.RLock() + defer it.storeMtx.RUnlock() + if it.index+1 >= len(it.rb.hashes) { return false } @@ -1903,7 +1965,10 @@ func (it *blockIterator) Next() bool { return true } -func (it *blockIterator) Prev() bool { +func (it *BlockIterator) Prev() bool { + it.storeMtx.RLock() + defer it.storeMtx.RUnlock() + if it.index-1 < 0 { return false } @@ -1911,10 +1976,13 @@ func (it *blockIterator) Prev() bool { return true } -func (it *blockIterator) BlockStamp() *BlockStamp { - return &BlockStamp{ +func (it *BlockIterator) BlockStamp() BlockStamp { + it.storeMtx.RLock() + defer it.storeMtx.RUnlock() + + return BlockStamp{ Height: it.rb.lastHeight - int32(len(it.rb.hashes)-1-it.index), - Hash: *it.rb.hashes[it.index], + Hash: it.rb.hashes[it.index], } } @@ -1923,21 +1991,21 @@ func (it *blockIterator) BlockStamp() *BlockStamp { // format. type unusedSpace struct { nBytes int // number of unused bytes that armory left. - rfvs []ReaderFromVersion + rfvs []readerFromVersion } -func newUnusedSpace(nBytes int, rfvs ...ReaderFromVersion) *unusedSpace { +func newUnusedSpace(nBytes int, rfvs ...readerFromVersion) *unusedSpace { return &unusedSpace{ nBytes: nBytes, rfvs: rfvs, } } -func (u *unusedSpace) ReadFromVersion(v version, r io.Reader) (int64, error) { +func (u *unusedSpace) readFromVersion(v version, r io.Reader) (int64, error) { var read int64 for _, rfv := range u.rfvs { - n, err := rfv.ReadFromVersion(v, r) + n, err := rfv.readFromVersion(v, r) if err != nil { return read + n, err } @@ -2124,7 +2192,7 @@ func newBtcAddressWithoutPrivkey(s *Store, pubkey, iv []byte, bs *BlockStamp) (a return nil, err } - address, err := btcutil.NewAddressPubKeyHash(btcutil.Hash160(pubkey), s.Net()) + address, err := btcutil.NewAddressPubKeyHash(btcutil.Hash160(pubkey), s.netParams()) if err != nil { return nil, err } @@ -2273,7 +2341,7 @@ func (a *btcAddress) ReadFrom(r io.Reader) (n int64, err error) { } a.pubKey = pk - addr, err := btcutil.NewAddressPubKeyHash(pubKeyHash[:], a.store.Net()) + addr, err := btcutil.NewAddressPubKeyHash(pubKeyHash[:], a.store.netParams()) if err != nil { return n, err } @@ -2509,7 +2577,7 @@ func (a *btcAddress) PrivKey() (*ecdsa.PrivateKey, error) { } // Key store must be unlocked to decrypt the private key. - if a.store.IsLocked() { + if a.store.isLocked() { return nil, ErrLocked } @@ -2538,7 +2606,8 @@ func (a *btcAddress) ExportPrivKey() (*btcutil.WIF, error) { // as our program's assumptions are so broken that this needs to be // caught immediately, and a stack trace here is more useful than // elsewhere. - wif, err := btcutil.NewWIF((*btcec.PrivateKey)(pk), a.store.Net(), a.Compressed()) + wif, err := btcutil.NewWIF((*btcec.PrivateKey)(pk), a.store.netParams(), + a.Compressed()) if err != nil { panic(err) } @@ -2725,14 +2794,14 @@ type ScriptAddress interface { // iv must be 16 bytes, or nil (in which case it is randomly generated). func newScriptAddress(s *Store, script []byte, bs *BlockStamp) (addr *scriptAddress, err error) { class, addresses, reqSigs, err := - btcscript.ExtractPkScriptAddrs(script, s.Net()) + btcscript.ExtractPkScriptAddrs(script, s.netParams()) if err != nil { return nil, err } scriptHash := btcutil.Hash160(script) - address, err := btcutil.NewAddressScriptHashFromHash(scriptHash, s.Net()) + address, err := btcutil.NewAddressScriptHashFromHash(scriptHash, s.netParams()) if err != nil { return nil, err } @@ -2804,7 +2873,7 @@ func (sa *scriptAddress) ReadFrom(r io.Reader) (n int64, err error) { } address, err := btcutil.NewAddressScriptHashFromHash(scriptHash[:], - sa.store.Net()) + sa.store.netParams()) if err != nil { return n, err } @@ -2816,7 +2885,7 @@ func (sa *scriptAddress) ReadFrom(r io.Reader) (n int64, err error) { } class, addresses, reqSigs, err := - btcscript.ExtractPkScriptAddrs(sa.script, sa.store.Net()) + btcscript.ExtractPkScriptAddrs(sa.script, sa.store.netParams()) if err != nil { return n, err } @@ -3174,134 +3243,10 @@ func (e *scriptEntry) ReadFrom(r io.Reader) (n int64, err error) { return n + read, err } -type addrCommentEntry struct { - pubKeyHash160 [ripemd160.Size]byte - comment []byte -} - -func (e *addrCommentEntry) address(net *btcnet.Params) (*btcutil.AddressPubKeyHash, error) { - return btcutil.NewAddressPubKeyHash(e.pubKeyHash160[:], net) -} - -func (e *addrCommentEntry) WriteTo(w io.Writer) (n int64, err error) { - var written int64 - - // Comments shall not overflow their entry. - if len(e.comment) > maxCommentLen { - return n, ErrMalformedEntry - } - - // Write header - if written, err = binaryWrite(w, binary.LittleEndian, addrCommentHeader); err != nil { - return n + written, err - } - n += written - - // Write hash - if written, err = binaryWrite(w, binary.LittleEndian, &e.pubKeyHash160); err != nil { - return n + written, err - } - n += written - - // Write length - if written, err = binaryWrite(w, binary.LittleEndian, uint16(len(e.comment))); err != nil { - return n + written, err - } - n += written - - // Write comment - written, err = binaryWrite(w, binary.LittleEndian, e.comment) - return n + written, err -} - -func (e *addrCommentEntry) ReadFrom(r io.Reader) (n int64, err error) { - var read int64 - - if read, err = binaryRead(r, binary.LittleEndian, &e.pubKeyHash160); err != nil { - return n + read, err - } - n += read - - var clen uint16 - if read, err = binaryRead(r, binary.LittleEndian, &clen); err != nil { - return n + read, err - } - n += read - - e.comment = make([]byte, clen) - read, err = binaryRead(r, binary.LittleEndian, e.comment) - return n + read, err -} - -type txCommentEntry struct { - txHash [sha256.Size]byte - comment []byte -} - -func (e *txCommentEntry) WriteTo(w io.Writer) (n int64, err error) { - var written int64 - - // Comments shall not overflow their entry. - if len(e.comment) > maxCommentLen { - return n, ErrMalformedEntry - } - - // Write header - if written, err = binaryWrite(w, binary.LittleEndian, txCommentHeader); err != nil { - return n + written, err - } - n += written - - // Write length - if written, err = binaryWrite(w, binary.LittleEndian, uint16(len(e.comment))); err != nil { - return n + written, err - } - - // Write comment - written, err = binaryWrite(w, binary.LittleEndian, e.comment) - return n + written, err -} - -func (e *txCommentEntry) ReadFrom(r io.Reader) (n int64, err error) { - var read int64 - - if read, err = binaryRead(r, binary.LittleEndian, &e.txHash); err != nil { - return n + read, err - } - n += read - - var clen uint16 - if read, err = binaryRead(r, binary.LittleEndian, &clen); err != nil { - return n + read, err - } - n += read - - e.comment = make([]byte, clen) - read, err = binaryRead(r, binary.LittleEndian, e.comment) - return n + read, err -} - -type deletedEntry struct{} - -func (e *deletedEntry) ReadFrom(r io.Reader) (n int64, err error) { - var read int64 - - var ulen uint16 - if read, err = binaryRead(r, binary.LittleEndian, &ulen); err != nil { - return n + read, err - } - n += read - - unused := make([]byte, ulen) - nRead, err := io.ReadFull(r, unused) - n += int64(nRead) - return n, err -} - // BlockStamp defines a block (by height and a unique hash) and is // used to mark a point in the blockchain that a key store element is // synced to. type BlockStamp struct { + Hash *btcwire.ShaHash Height int32 - Hash btcwire.ShaHash } diff --git a/keystore/keystore_test.go b/keystore/keystore_test.go index 753c300..cf48d77 100644 --- a/keystore/keystore_test.go +++ b/keystore/keystore_test.go @@ -28,12 +28,22 @@ import ( "github.com/conformal/btcnet" "github.com/conformal/btcscript" "github.com/conformal/btcutil" + "github.com/conformal/btcwire" "github.com/davecgh/go-spew/spew" ) +const dummyDir = "" + var tstNetParams = &btcnet.MainNetParams +func makeBS(height int32) *BlockStamp { + return &BlockStamp{ + Hash: new(btcwire.ShaHash), + Height: height, + } +} + func TestBtcAddressSerializer(t *testing.T) { fakeWallet := &Store{net: (*netParams)(tstNetParams)} kdfp := &kdfParameters{ @@ -44,14 +54,14 @@ func TestBtcAddressSerializer(t *testing.T) { t.Error(err.Error()) return } - key := Key([]byte("banana"), kdfp) + key := kdf([]byte("banana"), kdfp) privKey := make([]byte, 32) if _, err := rand.Read(privKey); err != nil { t.Error(err.Error()) return } addr, err := newBtcAddress(fakeWallet, privKey, nil, - &BlockStamp{}, true) + makeBS(0), true) if err != nil { t.Error(err.Error()) return @@ -91,7 +101,7 @@ func TestScriptAddressSerializer(t *testing.T) { fakeWallet := &Store{net: (*netParams)(tstNetParams)} script := []byte{btcscript.OP_TRUE, btcscript.OP_DUP, btcscript.OP_DROP} - addr, err := newScriptAddress(fakeWallet, script, &BlockStamp{}) + addr, err := newScriptAddress(fakeWallet, script, makeBS(0)) if err != nil { t.Error(err.Error()) return @@ -118,9 +128,9 @@ func TestScriptAddressSerializer(t *testing.T) { } func TestWalletCreationSerialization(t *testing.T) { - createdAt := &BlockStamp{} - w1, err := NewStore("banana wallet", "A wallet for testing.", - []byte("banana"), tstNetParams, createdAt, 100) + createdAt := makeBS(0) + w1, err := New(dummyDir, "A wallet for testing.", + []byte("banana"), tstNetParams, createdAt) if err != nil { t.Error("Error creating new wallet: " + err.Error()) return @@ -215,16 +225,16 @@ func TestChaining(t *testing.T) { // Create next chained private keys, chained from both the uncompressed // and compressed pubkeys. - nextPrivUncompressed, err := ChainedPrivKey(test.origPrivateKey, + nextPrivUncompressed, err := chainedPrivKey(test.origPrivateKey, origPubUncompressed, test.cc) if err != nil { - t.Errorf("%s: Uncompressed ChainedPrivKey failed: %v", test.name, err) + t.Errorf("%s: Uncompressed chainedPrivKey failed: %v", test.name, err) return } - nextPrivCompressed, err := ChainedPrivKey(test.origPrivateKey, + nextPrivCompressed, err := chainedPrivKey(test.origPrivateKey, origPubCompressed, test.cc) if err != nil { - t.Errorf("%s: Compressed ChainedPrivKey failed: %v", test.name, err) + t.Errorf("%s: Compressed chainedPrivKey failed: %v", test.name, err) return } @@ -247,14 +257,14 @@ func TestChaining(t *testing.T) { // Create the next pubkeys by chaining directly off the original // pubkeys (without using the original's private key). - nextPubUncompressedFromPub, err := ChainedPubKey(origPubUncompressed, test.cc) + nextPubUncompressedFromPub, err := chainedPubKey(origPubUncompressed, test.cc) if err != nil { - t.Errorf("%s: Uncompressed ChainedPubKey failed: %v", test.name, err) + t.Errorf("%s: Uncompressed chainedPubKey failed: %v", test.name, err) return } - nextPubCompressedFromPub, err := ChainedPubKey(origPubCompressed, test.cc) + nextPubCompressedFromPub, err := chainedPubKey(origPubCompressed, test.cc) if err != nil { - t.Errorf("%s: Compressed ChainedPubKey failed: %v", test.name, err) + t.Errorf("%s: Compressed chainedPubKey failed: %v", test.name, err) return } @@ -328,11 +338,8 @@ func TestChaining(t *testing.T) { } func TestWalletPubkeyChaining(t *testing.T) { - // Set a reasonable keypool size that isn't too big nor too small for testing. - const keypoolSize = 5 - - w, err := NewStore("banana wallet", "A wallet for testing.", - []byte("banana"), tstNetParams, &BlockStamp{}, keypoolSize) + w, err := New(dummyDir, "A wallet for testing.", + []byte("banana"), tstNetParams, makeBS(0)) if err != nil { t.Error("Error creating new wallet: " + err.Error()) return @@ -341,20 +348,9 @@ func TestWalletPubkeyChaining(t *testing.T) { t.Error("New wallet is not locked.") } - // Wallet should have a total of 6 addresses, one for the root, plus 5 in - // the keypool with their private keys set. Ask for as many new addresses - // as needed to deplete the pool. - for i := 0; i < keypoolSize; i++ { - _, err := w.NextChainedAddress(&BlockStamp{}, keypoolSize) - if err != nil { - t.Errorf("Error getting next address from keypool: %v", err) - return - } - } - - // Get next chained address after depleting the keypool. This will extend - // the chain based on the last pubkey, not privkey. - addrWithoutPrivkey, err := w.NextChainedAddress(&BlockStamp{}, keypoolSize) + // Get next chained address. The wallet is locked, so this will chain + // off the last pubkey, not privkey. + addrWithoutPrivkey, err := w.NextChainedAddress(makeBS(0)) if err != nil { t.Errorf("Failed to extend address chain from pubkey: %v", err) return @@ -456,13 +452,9 @@ func TestWalletPubkeyChaining(t *testing.T) { return } - // Test that normal keypool extension and address creation continues to - // work. With the wallet still unlocked, create a new address. This - // will cause the keypool to refill and return the first address from the - // keypool. - nextAddr, err := w.NextChainedAddress(&BlockStamp{}, keypoolSize) + nextAddr, err := w.NextChainedAddress(makeBS(0)) if err != nil { - t.Errorf("Unable to create next address or refill keypool after finding the privkey: %v", err) + t.Errorf("Unable to create next address after finding the privkey: %v", err) return } @@ -505,10 +497,9 @@ func TestWalletPubkeyChaining(t *testing.T) { } func TestWatchingWalletExport(t *testing.T) { - const keypoolSize = 10 - createdAt := &BlockStamp{} - w, err := NewStore("banana wallet", "A wallet for testing.", - []byte("banana"), tstNetParams, createdAt, keypoolSize) + createdAt := makeBS(0) + w, err := New(dummyDir, "A wallet for testing.", + []byte("banana"), tstNetParams, createdAt) if err != nil { t.Error("Error creating new wallet: " + err.Error()) return @@ -520,19 +511,6 @@ func TestWatchingWalletExport(t *testing.T) { // Add root address. activeAddrs[getAddressKey(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[getAddressKey(apkh)] = struct{}{} - } - // Create watching wallet from w. ww, err := w.ExportWatchingWallet() if err != nil { @@ -606,40 +584,23 @@ func TestWatchingWalletExport(t *testing.T) { } // 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. + // original wallet is unlocked so addresses are chained with privkeys. 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.EncodeAddress() != waddr.EncodeAddress() { - t.Errorf("Next addresses for each wallet do not match eachother.") - return - } - } // Test that ExtendActiveAddresses for the watching wallet match // manually requested addresses of the original wallet. - newAddrs := make([]btcutil.Address, 0, keypoolSize) - for i := 0; i < keypoolSize; i++ { - addr, err := w.NextChainedAddress(createdAt, keypoolSize) + var newAddrs []btcutil.Address + for i := 0; i < 10; i++ { + addr, err := w.NextChainedAddress(createdAt) if err != nil { t.Errorf("Cannot get next chained address for original wallet: %v", err) return } newAddrs = append(newAddrs, addr) } - newWWAddrs, err := ww.ExtendActiveAddresses(keypoolSize, keypoolSize) + newWWAddrs, err := ww.ExtendActiveAddresses(10) if err != nil { t.Errorf("Cannot extend active addresses for watching wallet: %v", err) return @@ -653,16 +614,16 @@ func TestWatchingWalletExport(t *testing.T) { // Test ExtendActiveAddresses for the original wallet after manually // requesting addresses for the watching wallet. - newWWAddrs = make([]btcutil.Address, 0, keypoolSize) - for i := 0; i < keypoolSize; i++ { - addr, err := ww.NextChainedAddress(createdAt, keypoolSize) + newWWAddrs = nil + for i := 0; i < 10; i++ { + addr, err := ww.NextChainedAddress(createdAt) if err != nil { t.Errorf("Cannot get next chained address for watching wallet: %v", err) return } newWWAddrs = append(newWWAddrs, addr) } - newAddrs, err = w.ExtendActiveAddresses(keypoolSize, keypoolSize) + newAddrs, err = w.ExtendActiveAddresses(10) if err != nil { t.Errorf("Cannot extend active addresses for original wallet: %v", err) return @@ -728,11 +689,10 @@ func TestWatchingWalletExport(t *testing.T) { } func TestImportPrivateKey(t *testing.T) { - const keypoolSize = 10 createHeight := int32(100) - createdAt := &BlockStamp{Height: createHeight} - w, err := NewStore("banana wallet", "A wallet for testing.", - []byte("banana"), tstNetParams, createdAt, keypoolSize) + createdAt := makeBS(createHeight) + w, err := New(dummyDir, "A wallet for testing.", + []byte("banana"), tstNetParams, createdAt) if err != nil { t.Error("Error creating new wallet: " + err.Error()) return @@ -751,7 +711,7 @@ func TestImportPrivateKey(t *testing.T) { // verify that the entire wallet's sync height matches the // expected createHeight. - if h := w.SyncHeight(); h != createHeight { + if _, h := w.SyncedTo(); h != createHeight { t.Errorf("Initial sync height %v does not match expected %v.", h, createHeight) return } @@ -762,7 +722,7 @@ func TestImportPrivateKey(t *testing.T) { t.Fatal(err) } importHeight := int32(50) - importedAt := &BlockStamp{Height: importHeight} + importedAt := makeBS(importHeight) address, err := w.ImportPrivateKey(wif, importedAt) if err != nil { t.Error("importing private key: " + err.Error()) @@ -788,7 +748,7 @@ func TestImportPrivateKey(t *testing.T) { } // verify that the sync height now match the (smaller) import height. - if h := w.SyncHeight(); h != importHeight { + if _, h := w.SyncedTo(); h != importHeight { t.Errorf("After import sync height %v does not match expected %v.", h, importHeight) return } @@ -810,7 +770,7 @@ func TestImportPrivateKey(t *testing.T) { } // Verify that the sync height match expected after the reserialization. - if h := w2.SyncHeight(); h != importHeight { + if _, h := w2.SyncedTo(); h != importHeight { t.Errorf("After reserialization sync height %v does not match expected %v.", h, importHeight) return } @@ -822,7 +782,7 @@ func TestImportPrivateKey(t *testing.T) { t.Errorf("Cannot mark address partially synced: %v", err) return } - if h := w2.SyncHeight(); h != partialHeight { + if _, h := w2.SyncedTo(); h != partialHeight { t.Errorf("After address partial sync, sync height %v does not match expected %v.", h, partialHeight) return } @@ -842,7 +802,7 @@ func TestImportPrivateKey(t *testing.T) { } // Test correct partial height after serialization. - if h := w3.SyncHeight(); h != partialHeight { + if _, h := w3.SyncedTo(); h != partialHeight { t.Errorf("After address partial sync and reserialization, sync height %v does not match expected %v.", h, partialHeight) return @@ -854,7 +814,7 @@ func TestImportPrivateKey(t *testing.T) { t.Errorf("Cannot mark address synced: %v", err) return } - if h := w3.SyncHeight(); h != importHeight { + if _, h := w3.SyncedTo(); h != importHeight { t.Errorf("After address unsync, sync height %v does not match expected %v.", h, importHeight) return } @@ -866,7 +826,7 @@ func TestImportPrivateKey(t *testing.T) { t.Errorf("Cannot mark address synced: %v", err) return } - if h := w3.SyncHeight(); h != createHeight { + if _, h := w3.SyncedTo(); h != createHeight { t.Errorf("After address sync, sync height %v does not match expected %v.", h, createHeight) return } @@ -898,11 +858,10 @@ func TestImportPrivateKey(t *testing.T) { } func TestImportScript(t *testing.T) { - const keypoolSize = 10 createHeight := int32(100) - createdAt := &BlockStamp{Height: createHeight} - w, err := NewStore("banana wallet", "A wallet for testing.", - []byte("banana"), tstNetParams, createdAt, keypoolSize) + createdAt := makeBS(createHeight) + w, err := New(dummyDir, "A wallet for testing.", + []byte("banana"), tstNetParams, createdAt) if err != nil { t.Error("Error creating new wallet: " + err.Error()) return @@ -915,7 +874,7 @@ func TestImportScript(t *testing.T) { // verify that the entire wallet's sync height matches the // expected createHeight. - if h := w.SyncHeight(); h != createHeight { + if _, h := w.SyncedTo(); h != createHeight { t.Errorf("Initial sync height %v does not match expected %v.", h, createHeight) return } @@ -923,7 +882,7 @@ func TestImportScript(t *testing.T) { script := []byte{btcscript.OP_TRUE, btcscript.OP_DUP, btcscript.OP_DROP} importHeight := int32(50) - stamp := &BlockStamp{Height: importHeight} + stamp := makeBS(importHeight) address, err := w.ImportScript(script, stamp) if err != nil { t.Error("error importing script: " + err.Error()) @@ -989,7 +948,7 @@ func TestImportScript(t *testing.T) { } // verify that the sync height now match the (smaller) import height. - if h := w.SyncHeight(); h != importHeight { + if _, h := w.SyncedTo(); h != importHeight { t.Errorf("After import sync height %v does not match expected %v.", h, importHeight) return } @@ -1028,7 +987,7 @@ func TestImportScript(t *testing.T) { } // Verify that the sync height matches expected after the reserialization. - if h := w2.SyncHeight(); h != importHeight { + if _, h := w2.SyncedTo(); h != importHeight { t.Errorf("After reserialization sync height %v does not match expected %v.", h, importHeight) return } @@ -1125,7 +1084,7 @@ func TestImportScript(t *testing.T) { t.Errorf("Cannot mark address partially synced: %v", err) return } - if h := w2.SyncHeight(); h != partialHeight { + if _, h := w2.SyncedTo(); h != partialHeight { t.Errorf("After address partial sync, sync height %v does not match expected %v.", h, partialHeight) return } @@ -1145,7 +1104,7 @@ func TestImportScript(t *testing.T) { } // Test correct partial height after serialization. - if h := w3.SyncHeight(); h != partialHeight { + if _, h := w3.SyncedTo(); h != partialHeight { t.Errorf("After address partial sync and reserialization, sync height %v does not match expected %v.", h, partialHeight) return @@ -1157,7 +1116,7 @@ func TestImportScript(t *testing.T) { t.Errorf("Cannot mark address synced: %v", err) return } - if h := w3.SyncHeight(); h != importHeight { + if _, h := w3.SyncedTo(); h != importHeight { t.Errorf("After address unsync, sync height %v does not match expected %v.", h, importHeight) return } @@ -1169,7 +1128,7 @@ func TestImportScript(t *testing.T) { t.Errorf("Cannot mark address synced: %v", err) return } - if h := w3.SyncHeight(); h != createHeight { + if _, h := w3.SyncedTo(); h != createHeight { t.Errorf("After address sync, sync height %v does not match expected %v.", h, createHeight) return } @@ -1181,10 +1140,9 @@ func TestImportScript(t *testing.T) { } func TestChangePassphrase(t *testing.T) { - const keypoolSize = 10 - createdAt := &BlockStamp{} - w, err := NewStore("banana wallet", "A wallet for testing.", - []byte("banana"), tstNetParams, createdAt, keypoolSize) + createdAt := makeBS(0) + w, err := New(dummyDir, "A wallet for testing.", + []byte("banana"), tstNetParams, createdAt) if err != nil { t.Error("Error creating new wallet: " + err.Error()) return diff --git a/log.go b/log.go index a39ab9b..e480b96 100644 --- a/log.go +++ b/log.go @@ -21,6 +21,7 @@ import ( "os" "github.com/conformal/btclog" + "github.com/conformal/btcwallet/chain" "github.com/conformal/btcwallet/txstore" "github.com/conformal/seelog" ) @@ -42,12 +43,14 @@ var ( backendLog = seelog.Disabled log = btclog.Disabled txstLog = btclog.Disabled + chainLog = btclog.Disabled ) // subsystemLoggers maps each subsystem identifier to its associated logger. var subsystemLoggers = map[string]btclog.Logger{ "BTCW": log, "TXST": txstLog, + "CHNS": chainLog, } // logClosure is used to provide a closure over expensive logging operations @@ -80,6 +83,9 @@ func useLogger(subsystemID string, logger btclog.Logger) { case "TXST": txstLog = logger txstore.UseLogger(logger) + case "CHNS": + chainLog = logger + chain.UseLogger(logger) } } diff --git a/rename_plan9.go b/rename/rename_plan9.go similarity index 93% rename from rename_plan9.go rename to rename/rename_plan9.go index a0992d6..6de293e 100644 --- a/rename_plan9.go +++ b/rename/rename_plan9.go @@ -26,16 +26,16 @@ // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -package main +package rename import ( "os" "path/filepath" ) -// Rename provides an atomic file rename. newpath is replaced if it +// Atomic provides an atomic file rename. newpath is replaced if it // already exists. -func Rename(oldpath, newpath string) error { +func Atomic(oldpath, newpath string) error { if _, err := os.Stat(newpath); err == nil { if err := os.Remove(newpath); err != nil { return err diff --git a/rename_unix.go b/rename/rename_unix.go similarity index 87% rename from rename_unix.go rename to rename/rename_unix.go index 6fb2a3f..c882a20 100644 --- a/rename_unix.go +++ b/rename/rename_unix.go @@ -14,14 +14,14 @@ // +build !windows,!plan9 -package main +package rename import ( "os" ) -// Rename provides an atomic file rename. newpath is replaced if it +// Atomic provides an atomic file rename. newpath is replaced if it // already exists. -func Rename(oldpath, newpath string) error { +func Atomic(oldpath, newpath string) error { return os.Rename(oldpath, newpath) } diff --git a/rename_windows.go b/rename/rename_windows.go similarity index 94% rename from rename_windows.go rename to rename/rename_windows.go index a976de4..5b5c953 100644 --- a/rename_windows.go +++ b/rename/rename_windows.go @@ -26,7 +26,7 @@ // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -package main +package rename import ( "syscall" @@ -56,9 +56,9 @@ func moveFileEx(from *uint16, to *uint16, flags uint32) error { return nil } -// Rename provides an atomic file rename. newpath is replaced if it +// Atomic provides an atomic file rename. newpath is replaced if it // already exists. -func Rename(oldpath, newpath string) error { +func Atomic(oldpath, newpath string) error { from, err := syscall.UTF16PtrFromString(oldpath) if err != nil { return err diff --git a/rescan.go b/rescan.go index 55da21b..37c2a61 100644 --- a/rescan.go +++ b/rescan.go @@ -17,282 +17,302 @@ package main import ( - "sync" - "github.com/conformal/btcutil" + "github.com/conformal/btcwallet/chain" + "github.com/conformal/btcwallet/keystore" "github.com/conformal/btcwire" ) -// RescanMsg is the interface type for messages sent to the -// RescanManager's message channel. -type RescanMsg interface { - ImplementsRescanMsg() -} - -// RescanStartedMsg reports the job being processed for a new -// rescan. -type RescanStartedMsg RescanJob - -// ImplementsRescanMsg is implemented to satisify the RescanMsg -// interface. -func (r *RescanStartedMsg) ImplementsRescanMsg() {} - -// RescanProgressMsg reports the current progress made by a rescan -// for a set of account's addresses. +// RescanProgressMsg reports the current progress made by a rescan for a +// set of wallet addresses. type RescanProgressMsg struct { - Addresses map[*Account][]btcutil.Address - Height int32 + Addresses []btcutil.Address + Notification *chain.RescanProgress } -// ImplementsRescanMsg is implemented to satisify the RescanMsg -// interface. -func (r *RescanProgressMsg) ImplementsRescanMsg() {} - -// RescanFinishedMsg reports the set of account's addresses of a -// possibly-finished rescan, or an error if the rescan failed. +// RescanFinishedMsg reports the addresses that were rescanned when a +// rescanfinished message was received rescanning a batch of addresses. type RescanFinishedMsg struct { - Addresses map[*Account][]btcutil.Address - Error error + Addresses []btcutil.Address + Notification *chain.RescanFinished + WasInitialSync bool } -// ImplementsRescanMsg is implemented to satisify the RescanMsg -// interface. -func (r *RescanFinishedMsg) ImplementsRescanMsg() {} - -// RescanManager manages a set of current and to be processed account's -// addresses, batching waiting jobs together to minimize the total time -// needed to rescan many separate jobs. Rescan requests are processed -// one at a time, and the next batch does not run until the current -// has finished. -type RescanManager struct { - addJob chan *RescanJob - sendJob chan *RescanJob - status chan interface{} // rescanProgress and rescanFinished - msgs chan RescanMsg - jobCompleteChan chan chan struct{} - wg sync.WaitGroup - quit chan struct{} -} - -// NewRescanManager creates a new RescanManger. If msgChan is non-nil, -// rescan messages are sent to the channel for additional processing by -// the caller. -func NewRescanManager(msgChan chan RescanMsg) *RescanManager { - return &RescanManager{ - addJob: make(chan *RescanJob, 1), - sendJob: make(chan *RescanJob, 1), - status: make(chan interface{}, 1), - msgs: msgChan, - jobCompleteChan: make(chan chan struct{}, 1), - quit: make(chan struct{}), - } -} - -// Start starts the goroutines to run the RescanManager. -func (m *RescanManager) Start() { - m.wg.Add(2) - go m.jobHandler() - go m.rpcHandler() -} - -func (m *RescanManager) Stop() { - close(m.quit) -} - -func (m *RescanManager) WaitForShutdown() { - m.wg.Wait() +// RescanJob is a job to be processed by the RescanManager. The job includes +// a set of wallet addresses, a starting height to begin the rescan, and +// outpoints spendable by the addresses thought to be unspent. After the +// rescan completes, the error result of the rescan RPC is sent on the Err +// channel. +type RescanJob struct { + InitialSync bool + Addrs []btcutil.Address + OutPoints []*btcwire.OutPoint + BlockStamp keystore.BlockStamp + err chan error } +// rescanBatch is a collection of one or more RescanJobs that were merged +// together before a rescan is performed. type rescanBatch struct { - addrs map[*Account][]btcutil.Address - outpoints map[btcwire.OutPoint]struct{} - height int32 - complete chan struct{} + initialSync bool + addrs []btcutil.Address + outpoints []*btcwire.OutPoint + bs keystore.BlockStamp + errChans []chan error } -func newRescanBatch() *rescanBatch { +// SubmitRescan submits a RescanJob to the RescanManager. A channel is +// returned with the final error of the rescan. The channel is buffered +// and does not need to be read to prevent a deadlock. +func (w *Wallet) SubmitRescan(job *RescanJob) <-chan error { + errChan := make(chan error, 1) + job.err = errChan + w.rescanAddJob <- job + return errChan +} + +// batch creates the rescanBatch for a single rescan job. +func (job *RescanJob) batch() *rescanBatch { return &rescanBatch{ - addrs: map[*Account][]btcutil.Address{}, - outpoints: map[btcwire.OutPoint]struct{}{}, - height: -1, - complete: make(chan struct{}), - } -} - -func (b *rescanBatch) done() { - close(b.complete) -} - -func (b *rescanBatch) empty() bool { - return len(b.addrs) == 0 -} - -func (b *rescanBatch) job() *RescanJob { - // Create slice of outpoints from the batch's set. - outpoints := make([]*btcwire.OutPoint, 0, len(b.outpoints)) - for outpoint := range b.outpoints { - opCopy := outpoint - outpoints = append(outpoints, &opCopy) - } - - return &RescanJob{ - Addresses: b.addrs, - OutPoints: outpoints, - StartHeight: b.height, + initialSync: job.InitialSync, + addrs: job.Addrs, + outpoints: job.OutPoints, + bs: job.BlockStamp, + errChans: []chan error{job.err}, } } +// merge merges the work from k into j, setting the starting height to +// the minimum of the two jobs. This method does not check for +// duplicate addresses or outpoints. func (b *rescanBatch) merge(job *RescanJob) { - for acct, addr := range job.Addresses { - b.addrs[acct] = append(b.addrs[acct], addr...) + if job.InitialSync { + b.initialSync = true } - for _, op := range job.OutPoints { - b.outpoints[*op] = struct{}{} + b.addrs = append(b.addrs, job.Addrs...) + b.outpoints = append(b.outpoints, job.OutPoints...) + if job.BlockStamp.Height < b.bs.Height { + b.bs = job.BlockStamp } - if b.height == -1 || job.StartHeight < b.height { - b.height = job.StartHeight + b.errChans = append(b.errChans, job.err) +} + +// done iterates through all error channels, duplicating sending the error +// to inform callers that the rescan finished (or could not complete due +// to an error). +func (b *rescanBatch) done(err error) { + for _, c := range b.errChans { + c <- err } } -// jobHandler runs the RescanManager's for-select loop to manage rescan jobs -// and dispatch requests. -func (m *RescanManager) jobHandler() { - curBatch := newRescanBatch() - nextBatch := newRescanBatch() +// rescanBatchHandler handles incoming rescan request, serializing rescan +// submissions, and possibly batching many waiting requests together so they +// can be handled by a single rescan after the current one completes. +func (w *Wallet) rescanBatchHandler() { + var curBatch, nextBatch *rescanBatch out: for { select { - case job := <-m.addJob: - if curBatch.empty() { + case job := <-w.rescanAddJob: + if curBatch == nil { // Set current batch as this job and send // request. - curBatch.merge(job) - m.sendJob <- job - - // Send the channel that is closed when the - // current batch completes. - m.jobCompleteChan <- curBatch.complete - - // Notify listener of a newly-started rescan. - if m.msgs != nil { - m.msgs <- (*RescanStartedMsg)(job) - } + curBatch = job.batch() + w.rescanBatch <- curBatch } else { - // Add job to waiting batch. - nextBatch.merge(job) - - // Send the channel that is closed when the - // waiting batch completes. - m.jobCompleteChan <- nextBatch.complete + // Create next batch if it doesn't exist, or + // merge the job. + if nextBatch == nil { + nextBatch = job.batch() + } else { + nextBatch.merge(job) + } } - case status := <-m.status: - switch s := status.(type) { - case rescanProgress: - if m.msgs != nil { - m.msgs <- &RescanProgressMsg{ - Addresses: curBatch.addrs, - Height: int32(s), - } + case n := <-w.rescanNotifications: + switch n := n.(type) { + case *chain.RescanProgress: + w.rescanProgress <- &RescanProgressMsg{ + Addresses: curBatch.addrs, + Notification: n, } - case rescanFinished: - if m.msgs != nil { - m.msgs <- &RescanFinishedMsg{ - Addresses: curBatch.addrs, - Error: s.error, - } + case *chain.RescanFinished: + if curBatch == nil { + log.Warnf("Received rescan finished " + + "notification but no rescan " + + "currently running") + continue + } + w.rescanFinished <- &RescanFinishedMsg{ + Addresses: curBatch.addrs, + Notification: n, + WasInitialSync: curBatch.initialSync, } - curBatch.done() - curBatch, nextBatch = nextBatch, newRescanBatch() + curBatch, nextBatch = nextBatch, nil - if !curBatch.empty() { - job := curBatch.job() - m.sendJob <- job - if m.msgs != nil { - m.msgs <- (*RescanStartedMsg)(job) - } + if curBatch != nil { + w.rescanBatch <- curBatch } default: - // Unexpected status message - panic(s) + // Unexpected message + panic(n) } - case <-m.quit: + case <-w.quit: break out } } - close(m.sendJob) - if m.msgs != nil { - close(m.msgs) - } - m.wg.Done() + + close(w.rescanBatch) + w.wg.Done() } -// rpcHandler reads jobs sent by the jobHandler and sends the rpc requests -// to perform the rescan. New jobs are not read until a rescan finishes. -// The jobHandler is notified when the processing the rescan finishes. -func (m *RescanManager) rpcHandler() { - for job := range m.sendJob { - var addrs []btcutil.Address - for _, accountAddrs := range job.Addresses { - addrs = append(addrs, accountAddrs...) +// rescanProgressHandler handles notifications for paritally and fully completed +// rescans by marking each rescanned address as partially or fully synced and +// writing the keystore back to disk. +func (w *Wallet) rescanProgressHandler() { +out: + for { + // These can't be processed out of order since both chans are + // unbuffured and are sent from same context (the batch + // handler). + select { + case msg := <-w.rescanProgress: + n := msg.Notification + log.Infof("Rescanned through block %v (height %d)", + n.Hash, n.Height) + + // TODO(jrick): save partial syncs should also include + // the block hash. + for _, addr := range msg.Addresses { + err := w.KeyStore.SetSyncStatus(addr, + keystore.PartialSync(n.Height)) + if err != nil { + log.Errorf("Error marking address %v "+ + "partially synced: %v", addr, err) + } + } + w.KeyStore.MarkDirty() + err := w.KeyStore.WriteIfDirty() + if err != nil { + log.Errorf("Could not write partial rescan "+ + "progress to keystore: %v", err) + } + + case msg := <-w.rescanFinished: + n := msg.Notification + addrs := msg.Addresses + noun := pickNoun(len(addrs), "address", "addresses") + if msg.WasInitialSync { + w.Track() + w.ResendUnminedTxs() + + bs := keystore.BlockStamp{ + Hash: n.Hash, + Height: n.Height, + } + w.KeyStore.SetSyncedWith(&bs) + w.notifyConnectedBlock(bs) + + // Mark wallet as synced to chain so connected + // and disconnected block notifications are + // processed. + close(w.chainSynced) + } + log.Infof("Finished rescan for %d %s (synced to block "+ + "%s, height %d)", len(addrs), noun, n.Hash, + n.Height) + + for _, addr := range addrs { + err := w.KeyStore.SetSyncStatus(addr, + keystore.FullSync{}) + if err != nil { + log.Errorf("Error marking address %v "+ + "fully synced: %v", addr, err) + } + } + w.KeyStore.MarkDirty() + err := w.KeyStore.WriteIfDirty() + if err != nil { + log.Errorf("Could not write finished rescan "+ + "progress to keystore: %v", err) + } + + case <-w.quit: + break out } - client, err := accessClient() + } + w.wg.Done() +} + +// rescanRPCHandler reads batch jobs sent by rescanBatchHandler and sends the +// RPC requests to perform a rescan. New jobs are not read until a rescan +// finishes. +func (w *Wallet) rescanRPCHandler() { + for batch := range w.rescanBatch { + // Log the newly-started rescan. + numAddrs := len(batch.addrs) + noun := pickNoun(numAddrs, "address", "addresses") + log.Infof("Started rescan from block %v (height %d) for %d %s", + batch.bs.Hash, batch.bs.Height, numAddrs, noun) + + err := w.chainSvr.Rescan(batch.bs.Hash, batch.addrs, + batch.outpoints) + if err != nil { + log.Errorf("Rescan for %d %s failed: %v", numAddrs, + noun, err) + } + batch.done(err) + } + w.wg.Done() +} + +// RescanActiveAddresses begins a rescan for all active addresses of a +// wallet. This is intended to be used to sync a wallet back up to the +// current best block in the main chain, and is considered an intial sync +// rescan. +func (w *Wallet) RescanActiveAddresses() (err error) { + // Determine the block necesary to start the rescan for all active + // addresses. + hash, height := w.KeyStore.SyncedTo() + if hash == nil { + // TODO: fix our "synced to block" handling (either in + // keystore or txstore, or elsewhere) so this *always* + // returns the block hash. Looking it up by height is + // asking for problems. + hash, err = w.chainSvr.GetBlockHash(int64(height)) if err != nil { - m.MarkFinished(rescanFinished{err}) return } - err = client.Rescan(job.StartHeight, addrs, job.OutPoints) - if err != nil { - m.MarkFinished(rescanFinished{err}) - } } - m.wg.Done() -} -// RescanJob is a job to be processed by the RescanManager. The job includes -// a set of account's addresses, a starting height to begin the rescan, and -// outpoints spendable by the addresses thought to be unspent. -type RescanJob struct { - Addresses map[*Account][]btcutil.Address - OutPoints []*btcwire.OutPoint - StartHeight int32 -} - -// Merge merges the work from k into j, setting the starting height to -// the minimum of the two jobs. This method does not check for -// duplicate addresses or outpoints. -func (j *RescanJob) Merge(k *RescanJob) { - for acct, addrs := range k.Addresses { - j.Addresses[acct] = append(j.Addresses[acct], addrs...) + actives := w.KeyStore.SortedActiveAddresses() + addrs := make([]btcutil.Address, len(actives)) + for i, addr := range actives { + addrs[i] = addr.Address() } - for _, op := range k.OutPoints { - j.OutPoints = append(j.OutPoints, op) + + unspents, err := w.TxStore.UnspentOutputs() + if err != nil { + return } - if k.StartHeight < j.StartHeight { - j.StartHeight = k.StartHeight + outpoints := make([]*btcwire.OutPoint, len(unspents)) + for i, output := range unspents { + outpoints[i] = output.OutPoint() } -} -// SubmitJob submits a RescanJob to the RescanManager. A channel is returned -// that is closed once the rescan request for the job completes. -func (m *RescanManager) SubmitJob(job *RescanJob) <-chan struct{} { - m.addJob <- job - return <-m.jobCompleteChan -} + job := &RescanJob{ + InitialSync: true, + Addrs: addrs, + OutPoints: outpoints, + BlockStamp: keystore.BlockStamp{Hash: hash, Height: height}, + } -// MarkProgress messages the RescanManager with the height of the block -// last processed by a running rescan. -func (m *RescanManager) MarkProgress(height rescanProgress) { - m.status <- height -} - -// MarkFinished messages the RescanManager that the currently running rescan -// finished, or errored prematurely. -func (m *RescanManager) MarkFinished(finished rescanFinished) { - m.status <- finished + // Submit merged job and block until rescan completes. + return <-w.SubmitRescan(job) } diff --git a/rpcclient.go b/rpcclient.go deleted file mode 100644 index 4a8d582..0000000 --- a/rpcclient.go +++ /dev/null @@ -1,565 +0,0 @@ -/* - * Copyright (c) 2013, 2014 Conformal Systems LLC - * - * Permission to use, copy, modify, and distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - */ - -package main - -import ( - "errors" - "fmt" - "sync" - "time" - - "github.com/conformal/btcrpcclient" - "github.com/conformal/btcscript" - "github.com/conformal/btcutil" - "github.com/conformal/btcwallet/keystore" - "github.com/conformal/btcwallet/txstore" - "github.com/conformal/btcwire" - "github.com/conformal/btcws" -) - -// InvalidNotificationError describes an error due to an invalid chain server -// notification and should be warned by wallet, but does not indicate an -// problem with the current wallet state. -type InvalidNotificationError struct { - error -} - -var ( - // MismatchingNetworks represents an error where a client connection - // to btcd cannot succeed due to btcwallet and btcd operating on - // different bitcoin networks. - ErrMismatchedNets = errors.New("mismatched networks") -) - -const ( - // maxConcurrentClientRequests is the maximum number of - // unhandled/running requests that the server will run for a websocket - // client at a time. Beyond this limit, additional request reads will - // block until a running request handler finishes. This limit exists to - // prevent a single connection from causing a denial of service attack - // with an unnecessarily large number of requests. - maxConcurrentClientRequests = 20 -) - -type blockSummary struct { - hash *btcwire.ShaHash - height int32 -} - -type acceptedTx struct { - tx *btcutil.Tx - block *btcws.BlockDetails // nil if unmined -} - -// Notification types. These are defined here and processed from from reading -// a notificationChan to avoid handling these notifications directly in -// btcrpcclient callbacks, which isn't very go-like and doesn't allow -// blocking client calls. -type ( - // Container type for any notification. - notification interface { - handleNotification(*rpcClient) error - } - - blockConnected blockSummary - blockDisconnected blockSummary - recvTx acceptedTx - redeemingTx acceptedTx - rescanFinished struct { - error - } - rescanProgress int32 -) - -func (n blockConnected) handleNotification(c *rpcClient) error { - // Update the blockstamp for the newly-connected block. - bs := keystore.BlockStamp{ - Height: n.height, - Hash: *n.hash, - } - c.mtx.Lock() - c.blockStamp = bs - c.mtx.Unlock() - - AcctMgr.Grab() - AcctMgr.BlockNotify(&bs) - AcctMgr.Release() - - // Pass notification to wallet clients too. - if server != nil { - // TODO: marshaling should be perfomred by the server, and - // sent only to client that have requested the notification. - marshaled, err := n.MarshalJSON() - // The parsed notification is expected to be marshalable. - if err != nil { - panic(err) - } - server.broadcasts <- marshaled - } - - return nil -} - -// MarshalJSON creates the JSON encoding of the chain notification to pass -// to any connected wallet clients. This should never error. -func (n blockConnected) MarshalJSON() ([]byte, error) { - nn := btcws.NewBlockConnectedNtfn(n.hash.String(), n.height) - return nn.MarshalJSON() -} - -func (n blockDisconnected) handleNotification(c *rpcClient) error { - AcctMgr.Grab() - defer AcctMgr.Release() - - // Rollback Utxo and Tx data stores. - if err := AcctMgr.Rollback(n.height, n.hash); err != nil { - return err - } - - // Pass notification to wallet clients too. - if server != nil { - // TODO: marshaling should be perfomred by the server, and - // sent only to client that have requested the notification. - marshaled, err := n.MarshalJSON() - // A btcws.BlockDisconnectedNtfn is expected to marshal without error. - // If it does, it indicates that one of its struct fields is of a - // non-marshalable type. - if err != nil { - panic(err) - } - server.broadcasts <- marshaled - } - - return nil -} - -// MarshalJSON creates the JSON encoding of the chain notification to pass -// to any connected wallet clients. This should never error. -func (n blockDisconnected) MarshalJSON() ([]byte, error) { - nn := btcws.NewBlockDisconnectedNtfn(n.hash.String(), n.height) - return nn.MarshalJSON() -} - -func parseBlock(block *btcws.BlockDetails) (*txstore.Block, int, error) { - if block == nil { - return nil, btcutil.TxIndexUnknown, nil - } - blksha, err := btcwire.NewShaHashFromStr(block.Hash) - if err != nil { - return nil, btcutil.TxIndexUnknown, err - } - b := &txstore.Block{ - Height: block.Height, - Hash: *blksha, - Time: time.Unix(block.Time, 0), - } - return b, block.Index, nil -} - -func (n recvTx) handleNotification(c *rpcClient) error { - block, txIdx, err := parseBlock(n.block) - if err != nil { - return InvalidNotificationError{err} - } - n.tx.SetIndex(txIdx) - - bs, err := c.BlockStamp() - if err != nil { - return fmt.Errorf("cannot get current block: %v", err) - } - - AcctMgr.Grab() - defer AcctMgr.Release() - - // For every output, if it pays to a wallet address, insert the - // transaction into the store (possibly moving it from unconfirmed to - // confirmed), and add a credit record if one does not already exist. - var txr *txstore.TxRecord - txInserted := false - for i, txout := range n.tx.MsgTx().TxOut { - // Errors don't matter here. If addrs is nil, the range below - // does nothing. - _, addrs, _, _ := btcscript.ExtractPkScriptAddrs(txout.PkScript, - activeNet.Params) - for _, addr := range addrs { - a, err := AcctMgr.AccountByAddress(addr) - if err != nil { - continue // try next address, if any - } - - if !txInserted { - txr, err = a.TxStore.InsertTx(n.tx, block) - if err != nil { - return err - } - txInserted = true - } - - // Insert and notify websocket clients of the credit if it is - // not a duplicate, otherwise, check the next txout if the - // credit has already been inserted. - if txr.HasCredit(i) { - break - } - cred, err := txr.AddCredit(uint32(i), false) - if err != nil { - return err - } - AcctMgr.ds.ScheduleTxStoreWrite(a) - ltr, err := cred.ToJSON(a.KeyStore.Name(), bs.Height, - a.KeyStore.Net()) - if err != nil { - return err - } - server.NotifyNewTxDetails(a.KeyStore.Name(), ltr) - break // check whether next txout is a wallet txout - } - } - server.NotifyBalances() - - return nil -} - -func (n redeemingTx) handleNotification(c *rpcClient) error { - block, txIdx, err := parseBlock(n.block) - if err != nil { - return InvalidNotificationError{err} - } - n.tx.SetIndex(txIdx) - - AcctMgr.Grab() - err = AcctMgr.RecordSpendingTx(n.tx, block) - AcctMgr.Release() - return err -} - -func (n rescanFinished) handleNotification(c *rpcClient) error { - AcctMgr.rm.MarkFinished(n) - return nil -} - -func (n rescanProgress) handleNotification(c *rpcClient) error { - AcctMgr.rm.MarkProgress(n) - return nil -} - -type rpcClient struct { - *btcrpcclient.Client // client to btcd - - mtx sync.Mutex - blockStamp keystore.BlockStamp - - enqueueNotification chan notification - dequeueNotification chan notification - - quit chan struct{} - wg sync.WaitGroup -} - -func newRPCClient(certs []byte) (*rpcClient, error) { - client := rpcClient{ - blockStamp: keystore.BlockStamp{ - Height: int32(btcutil.BlockHeightUnknown), - }, - enqueueNotification: make(chan notification), - dequeueNotification: make(chan notification), - quit: make(chan struct{}), - } - initializedClient := make(chan struct{}) - ntfnCallbacks := btcrpcclient.NotificationHandlers{ - OnClientConnected: func() { - log.Info("Established connection to btcd") - <-initializedClient - - // nil client to broadcast to all connected clients - server.NotifyConnectionStatus(nil) - - err := client.Handshake() - if err != nil { - log.Errorf("Cannot complete handshake: %v", err) - client.Stop() - } - }, - OnBlockConnected: client.onBlockConnected, - OnBlockDisconnected: client.onBlockDisconnected, - OnRecvTx: client.onRecvTx, - OnRedeemingTx: client.onRedeemingTx, - OnRescanFinished: client.onRescanFinished, - OnRescanProgress: client.onRescanProgress, - } - conf := btcrpcclient.ConnConfig{ - Host: cfg.RPCConnect, - Endpoint: "ws", - User: cfg.BtcdUsername, - Pass: cfg.BtcdPassword, - Certificates: certs, - } - c, err := btcrpcclient.New(&conf, &ntfnCallbacks) - if err != nil { - return nil, err - } - client.Client = c - close(initializedClient) - return &client, nil -} - -func (c *rpcClient) Start() { - c.wg.Add(2) - go c.notificationQueue() - go c.handleNotifications() -} - -func (c *rpcClient) Stop() { - log.Warn("Disconnecting chain server client connection") - c.Client.Shutdown() - - select { - case <-c.quit: - default: - close(c.quit) - close(c.enqueueNotification) - } -} - -func (c *rpcClient) WaitForShutdown() { - c.Client.WaitForShutdown() - c.wg.Wait() -} - -func (c *rpcClient) onBlockConnected(hash *btcwire.ShaHash, height int32) { - c.enqueueNotification <- (blockConnected)(blockSummary{hash, height}) -} - -func (c *rpcClient) onBlockDisconnected(hash *btcwire.ShaHash, height int32) { - c.enqueueNotification <- (blockDisconnected)(blockSummary{hash, height}) -} - -func (c *rpcClient) onRecvTx(tx *btcutil.Tx, block *btcws.BlockDetails) { - c.enqueueNotification <- recvTx{tx, block} -} - -func (c *rpcClient) onRedeemingTx(tx *btcutil.Tx, block *btcws.BlockDetails) { - c.enqueueNotification <- redeemingTx{tx, block} -} - -func (c *rpcClient) onRescanProgress(height int32) { - c.enqueueNotification <- rescanProgress(height) -} - -func (c *rpcClient) onRescanFinished(height int32) { - c.enqueueNotification <- rescanFinished{error: nil} -} - -func (c *rpcClient) notificationQueue() { - // TODO: Rather than leaving this as an unbounded queue for all types of - // notifications, try dropping ones where a later enqueued notification - // can fully invalidate one waiting to be processed. For example, - // blockconnected notifications for greater block heights can remove the - // need to process earlier blockconnected notifications still waiting - // here. - - var q []notification - enqueue := c.enqueueNotification - var dequeue chan notification - var next notification -out: - for { - select { - case n, ok := <-enqueue: - if !ok { - // If no notifications are queued for handling, - // the queue is finished. - if len(q) == 0 { - break out - } - // nil channel so no more reads can occur. - enqueue = nil - continue - } - if len(q) == 0 { - next = n - dequeue = c.dequeueNotification - } - q = append(q, n) - - case dequeue <- next: - q[0] = nil - q = q[1:] - if len(q) != 0 { - next = q[0] - } else { - // If no more notifications can be enqueued, the - // queue is finished. - if enqueue == nil { - break out - } - dequeue = nil - } - } - } - close(c.dequeueNotification) - c.wg.Done() -} - -func (c *rpcClient) handleNotifications() { - for n := range c.dequeueNotification { - err := n.handleNotification(c) - if err != nil { - switch e := err.(type) { - case InvalidNotificationError: - log.Warnf("Ignoring invalid notification: %v", e) - default: - log.Errorf("Cannot handle notification: %v", e) - } - } - } - c.wg.Done() -} - -// BlockStamp returns (as a blockstamp) the height and hash of the last seen -// block from the RPC client. If no blocks have been seen (the height is -1), -// the chain server is queried for the block and the result is saved for future -// calls, or an error is returned if the RPC is unsuccessful. -func (c *rpcClient) BlockStamp() (keystore.BlockStamp, error) { - c.mtx.Lock() - defer c.mtx.Unlock() - - if c.blockStamp.Height != int32(btcutil.BlockHeightUnknown) { - return c.blockStamp, nil - } - - hash, height, err := c.GetBestBlock() - if err != nil { - return keystore.BlockStamp{}, err - } - bs := keystore.BlockStamp{ - Hash: *hash, - Height: height, - } - c.blockStamp = bs - return bs, nil -} - -// Handshake first checks that the websocket connection between btcwallet and -// btcd is valid, that is, that there are no mismatching settings between -// the two processes (such as running on different Bitcoin networks). If the -// sanity checks pass, all wallets are set to be tracked against chain -// notifications from this btcd connection. -// -// TODO(jrick): Track and Rescan commands should be replaced with a -// single TrackSince function (or similar) which requests address -// notifications and performs the rescan since some block height. -func (c *rpcClient) Handshake() error { - net, err := c.GetCurrentNet() - if err != nil { - return err - } - if net != activeNet.Net { - return ErrMismatchedNets - } - - // Request notifications for connected and disconnected blocks. - if err := c.NotifyBlocks(); err != nil { - return err - } - - // Get current best block. If this is before than the oldest - // saved block hash, assume that this btcd instance is not yet - // synced up to a previous btcd that was last used with this - // wallet. - bs, err := c.BlockStamp() - if err != nil { - return err - } - if server != nil { - server.NotifyNewBlockChainHeight(&bs) - server.NotifyBalances() - } - - // Get default account. Only the default account is used to - // track recently-seen blocks. - a, err := AcctMgr.Account("") - if err != nil { - // No account yet is not a handshake error, but means our - // handshake is done. - return nil - } - - // TODO(jrick): if height is less than the earliest-saved block - // height, should probably wait for btcd to catch up. - - // Check that there was not any reorgs done since last connection. - // If so, rollback and rescan to catch up. - it := a.KeyStore.NewIterateRecentBlocks() - for cont := it != nil; cont; cont = it.Prev() { - bs := it.BlockStamp() - log.Debugf("Checking for previous saved block with height %v hash %v", - bs.Height, bs.Hash) - - if _, err := c.GetBlock(&bs.Hash); err != nil { - continue - } - - log.Debug("Found matching block.") - - // If we had to go back to any previous blocks (it.Next - // returns true), then rollback the next and all child blocks. - // This rollback is done here instead of in the blockMissing - // check above for each removed block because Rollback will - // try to write new tx and utxo files on each rollback. - if it.Next() { - bs := it.BlockStamp() - err := AcctMgr.Rollback(bs.Height, &bs.Hash) - if err != nil { - return err - } - } - - // Set default account to be marked in sync with the current - // blockstamp. This invalidates the iterator. - a.KeyStore.SetSyncedWith(bs) - - // Begin tracking wallets against this btcd instance. - AcctMgr.Track() - if err := AcctMgr.RescanActiveAddresses(nil); err != nil { - return err - } - // TODO: Only begin tracking new unspent outputs as a result - // of the rescan. This is also pretty racy, as a new block - // could arrive between rescan and by the time the new outpoint - // is added to btcd's websocket's unspent output set. - AcctMgr.Track() - - // (Re)send any unmined transactions to btcd in case of a btcd restart. - AcctMgr.ResendUnminedTxs() - - // Get current blockchain height and best block hash. - return nil - } - - // Iterator was invalid (wallet has never been synced) or there was a - // huge chain fork + reorg (more than 20 blocks). - AcctMgr.Track() - if err := AcctMgr.RescanActiveAddresses(&bs); err != nil { - return err - } - // TODO: only begin tracking new unspent outputs as a result of the - // rescan. This is also racy (see comment for second Track above). - AcctMgr.Track() - AcctMgr.ResendUnminedTxs() - return nil -} diff --git a/rpcserver.go b/rpcserver.go index 05a0687..ce51bcd 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -43,6 +43,7 @@ import ( "github.com/conformal/btcrpcclient" "github.com/conformal/btcscript" "github.com/conformal/btcutil" + "github.com/conformal/btcwallet/chain" "github.com/conformal/btcwallet/keystore" "github.com/conformal/btcwallet/txstore" "github.com/conformal/btcwire" @@ -92,38 +93,75 @@ var ( ErrAddressNotInWallet = InvalidAddressOrKeyError{ errors.New("address not found in wallet"), } + + ErrNoAccountSupport = btcjson.Error{ + Code: btcjson.ErrWalletInvalidAccountName.Code, + Message: "btcwallet does not support non-default accounts", + } + + ErrUnloadedWallet = btcjson.Error{ + Code: btcjson.ErrWallet.Code, + Message: "Request requires a wallet but wallet has not loaded yet", + } + + ErrNeedsChainSvr = btcjson.Error{ + Code: btcjson.ErrWallet.Code, + Message: "Request requires chain connected chain server", + } ) +// TODO(jrick): There are several error paths which 'replace' various errors +// with a more appropiate error from the btcjson package. Create a map of +// these replacements so they can be handled once after an RPC handler has +// returned and before the error is marshaled. + +// checkAccountName verifies that the passed account name is for the default +// account or '*' to represent all accounts. This is necessary to return +// errors to RPC clients for invalid account names, as account support is +// currently missing from btcwallet. +func checkAccountName(account string) error { + if account != "" && account != "*" { + return ErrNoAccountSupport + } + return nil +} + +// checkDefaultAccount verifies that the passed account name is the default +// account. This is necessary to return errors to RPC clients for invalid +// account names, as account support is currently missing from btcwallet. +func checkDefaultAccount(account string) error { + if account != "" { + return ErrNoAccountSupport + } + return nil +} + type websocketClient struct { - conn *websocket.Conn - authenticated bool - remoteAddr string - allRequests chan []byte - unauthedRequests chan unauthedRequest - responses chan []byte - quit chan struct{} // closed on disconnect + conn *websocket.Conn + authenticated bool + remoteAddr string + allRequests chan []byte + responses chan []byte + quit chan struct{} // closed on disconnect } func newWebsocketClient(c *websocket.Conn, authenticated bool, remoteAddr string) *websocketClient { return &websocketClient{ - conn: c, - authenticated: authenticated, - remoteAddr: remoteAddr, - allRequests: make(chan []byte), - unauthedRequests: make(chan unauthedRequest, maxConcurrentClientRequests), - responses: make(chan []byte), - quit: make(chan struct{}), + conn: c, + authenticated: authenticated, + remoteAddr: remoteAddr, + allRequests: make(chan []byte), + responses: make(chan []byte), + quit: make(chan struct{}), } } -var errDisconnected = errors.New("websocket client disconnected") - func (c *websocketClient) send(b []byte) error { select { case c.responses <- b: return nil case <-c.quit: - return errDisconnected + return errors.New("websocket client disconnected") } } @@ -183,7 +221,7 @@ func genCertPair(certFile, keyFile string) error { // Generate cert pair. org := "btcwallet autogenerated cert" - validUntil := time.Now().Add(10 * 365 * 24 * time.Hour) + validUntil := time.Now().Add(time.Hour * 24 * 365 * 10) cert, key, err := btcutil.NewTLSCertPair(org, validUntil, nil) if err != nil { return err @@ -207,43 +245,73 @@ func genCertPair(certFile, keyFile string) error { // rpcServer holds the items the RPC server may need to access (auth, // config, shutdown, etc.) type rpcServer struct { - wg sync.WaitGroup - maxClients int64 // Maximum number of concurrent active RPC HTTP clients - maxWebsockets int64 // Maximum number of concurrent active RPC WS clients - listeners []net.Listener - authsha [sha256.Size]byte - wsClients map[*websocketClient]struct{} + wallet *Wallet + chainSvr *chain.Client + createOK bool + handlerLookup func(string) (requestHandler, bool) + handlerLock sync.Locker - upgrader websocket.Upgrader + listeners []net.Listener + authsha [sha256.Size]byte + upgrader websocket.Upgrader - requests chan handlerJob + maxPostClients int64 // Max concurrent HTTP POST clients. + maxWebsocketClients int64 // Max concurrent websocket clients. - addWSClient chan *websocketClient - removeWSClient chan *websocketClient - broadcasts chan []byte + // Channels to register or unregister a websocket client for + // websocket notifications. + registerWSC chan *websocketClient + unregisterWSC chan *websocketClient + + // Channels read from other components from which notifications are + // created. + connectedBlocks <-chan keystore.BlockStamp + disconnectedBlocks <-chan keystore.BlockStamp + newCredits <-chan txstore.Credit + newDebits <-chan txstore.Debits + minedCredits <-chan txstore.Credit + minedDebits <-chan txstore.Debits + keystoreLocked <-chan bool + confirmedBalance <-chan btcutil.Amount + unconfirmedBalance <-chan btcutil.Amount + chainServerConnected <-chan bool + registerWalletNtfns chan struct{} + + // enqueueNotification and dequeueNotification handle both sides of an + // infinitly growing queue for websocket client notifications. + enqueueNotification chan wsClientNotification + dequeueNotification chan wsClientNotification + + // notificationHandlerQuit is closed when the notification handler + // goroutine shuts down. After this is closed, no more notifications + // will be sent to any websocket client response channel. notificationHandlerQuit chan struct{} - quit chan struct{} + wg sync.WaitGroup + quit chan struct{} + quitMtx sync.Mutex } // newRPCServer creates a new server for serving RPC client connections, both // HTTP POST and websocket. -func newRPCServer(listenAddrs []string, maxClients, maxWebsockets int64) (*rpcServer, error) { +func newRPCServer(listenAddrs []string, maxPost, maxWebsockets int64) (*rpcServer, error) { login := cfg.Username + ":" + cfg.Password auth := "Basic " + base64.StdEncoding.EncodeToString([]byte(login)) s := rpcServer{ - authsha: sha256.Sum256([]byte(auth)), - maxClients: maxClients, - maxWebsockets: maxWebsockets, - wsClients: map[*websocketClient]struct{}{}, + handlerLookup: unloadedWalletHandlerFunc, + handlerLock: new(sync.Mutex), + authsha: sha256.Sum256([]byte(auth)), + maxPostClients: maxPost, + maxWebsocketClients: maxWebsockets, upgrader: websocket.Upgrader{ // Allow all origins. CheckOrigin: func(r *http.Request) bool { return true }, }, - requests: make(chan handlerJob), - addWSClient: make(chan *websocketClient), - removeWSClient: make(chan *websocketClient), - broadcasts: make(chan []byte), + registerWSC: make(chan *websocketClient), + unregisterWSC: make(chan *websocketClient), + registerWalletNtfns: make(chan struct{}), + enqueueNotification: make(chan wsClientNotification), + dequeueNotification: make(chan wsClientNotification), notificationHandlerQuit: make(chan struct{}), quit: make(chan struct{}), } @@ -299,12 +367,10 @@ func newRPCServer(listenAddrs []string, maxClients, maxWebsockets int64) (*rpcSe // Start starts a HTTP server to provide standard RPC and extension // websocket connections for any number of btcwallet clients. func (s *rpcServer) Start() { - // A duplicator for notifications intended for all clients runs - // in another goroutines. Any such notifications are sent to - // the allClients channel and then sent to each connected client. - s.wg.Add(2) - go s.NotificationHandler() - go s.RequestHandler() + s.wg.Add(3) + go s.notificationListener() + go s.notificationQueue() + go s.notificationHandler() log.Trace("Starting RPC server") @@ -319,8 +385,8 @@ func (s *rpcServer) Start() { ReadTimeout: time.Second * rpcAuthTimeoutSeconds, } - serveMux.Handle("/", - throttledFn(s.maxClients, func(w http.ResponseWriter, r *http.Request) { + serveMux.Handle("/", throttledFn(s.maxPostClients, + func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Connection", "close") w.Header().Set("Content-Type", "application/json") r.Close = true @@ -330,12 +396,13 @@ func (s *rpcServer) Start() { http.Error(w, "401 Unauthorized.", http.StatusUnauthorized) return } + s.wg.Add(1) s.PostClientRPC(w, r) - }), - ) + s.wg.Done() + })) - serveMux.Handle("/ws", - throttledFn(s.maxWebsockets, func(w http.ResponseWriter, r *http.Request) { + serveMux.Handle("/ws", throttledFn(s.maxWebsocketClients, + func(w http.ResponseWriter, r *http.Request) { authenticated := false switch s.checkAuthHeader(r) { case nil: @@ -359,8 +426,7 @@ func (s *rpcServer) Start() { } wsc := newWebsocketClient(conn, authenticated, r.RemoteAddr) s.WebsocketClientRPC(wsc) - }), - ) + })) for _, listener := range s.listeners { s.wg.Add(1) @@ -377,20 +443,28 @@ func (s *rpcServer) Start() { // clients, disconnecting the chain server connection, and closing the wallet's // account files. func (s *rpcServer) Stop() { - // If the server is changed to run more than one rpc handler at a time, - // to prevent a double channel close, this should be replaced with an - // atomic test-and-set. + s.quitMtx.Lock() + defer s.quitMtx.Unlock() + select { case <-s.quit: - log.Warnf("Server already shutting down") return default: } log.Warn("Server shutting down") - // Stop all the listeners. There will not be any listeners if - // listening is disabled. + // Stop the connected wallet and chain server, if any. + s.handlerLock.Lock() + if s.wallet != nil { + s.wallet.Stop() + } + if s.chainSvr != nil { + s.chainSvr.Stop() + } + s.handlerLock.Unlock() + + // Stop all the listeners. for _, listener := range s.listeners { err := listener.Close() if err != nil { @@ -399,24 +473,126 @@ func (s *rpcServer) Stop() { } } - // Disconnect the connected chain server, if any. - rpcc, err := accessClient() - if err == nil { - rpcc.Stop() - } - - // Stop the account manager and finish all pending account file writes. - AcctMgr.Stop() - // Signal the remaining goroutines to stop. close(s.quit) } func (s *rpcServer) WaitForShutdown() { - AcctMgr.WaitForShutdown() + // First wait for the wallet and chain server to stop, if they + // were ever set. + s.handlerLock.Lock() + if s.wallet != nil { + s.wallet.WaitForShutdown() + } + if s.chainSvr != nil { + s.chainSvr.WaitForShutdown() + } + s.handlerLock.Unlock() + s.wg.Wait() } +type noopLocker struct{} + +func (noopLocker) Lock() {} +func (noopLocker) Unlock() {} + +// SetWallet sets the wallet dependency component needed to run a fully +// functional bitcoin wallet RPC server. If wallet is nil, this informs the +// server that the createencryptedwallet RPC method is valid and must be called +// by a client before any other wallet methods are allowed. +func (s *rpcServer) SetWallet(wallet *Wallet) { + s.handlerLock.Lock() + defer s.handlerLock.Unlock() + + if wallet == nil { + s.handlerLookup = missingWalletHandlerFunc + s.createOK = true + return + } + + s.wallet = wallet + s.registerWalletNtfns <- struct{}{} + + if s.chainSvr != nil { + // If the chain server rpc client is also set, there's no reason + // to keep the mutex around. Make the locker simply execute + // noops instead. + s.handlerLock = noopLocker{} + + // With both the wallet and chain server set, all handlers are + // ok to run. + s.handlerLookup = lookupAnyHandler + } +} + +// SetChainServer sets the chain server client component needed to run a fully +// functional bitcoin wallet RPC server. This should be set even before the +// client is connected, as any request handlers should return the error for +// a never connected client, rather than panicking (or never being looked up) +// if the client was never conneceted and added. +func (s *rpcServer) SetChainServer(chainSvr *chain.Client) { + s.handlerLock.Lock() + defer s.handlerLock.Unlock() + + s.chainSvr = chainSvr + if s.wallet != nil { + // If the wallet had already been set, there's no reason to keep + // the mutex around. Make the locker simply execute noops + // instead. + s.handlerLock = noopLocker{} + + // With both the chain server and wallet set, all handlers are + // ok to run. + s.handlerLookup = lookupAnyHandler + } +} + +// HandlerClosure creates a closure function for handling requests of the given +// method. This may be a request that is handled directly by btcwallet, or +// a chain server request that is handled by passing the request down to btcd. +// +// NOTE: These handlers do not handle special cases, such as the authenticate +// and createencryptedwallet methods. Each of these must be checked +// beforehand (the method is already known) and handled accordingly. +func (s *rpcServer) HandlerClosure(method string) requestHandlerClosure { + s.handlerLock.Lock() + defer s.handlerLock.Unlock() + + // With the lock held, make copies of these pointers for the closure. + wallet := s.wallet + chainSvr := s.chainSvr + + if handler, ok := s.handlerLookup(method); ok { + return func(request []byte, raw *rawRequest) btcjson.Reply { + cmd, err := btcjson.ParseMarshaledCmd(request) + if err != nil { + return makeResponse(raw.ID, nil, + btcjson.ErrInvalidRequest) + } + + result, err := handler(wallet, chainSvr, cmd) + return makeResponse(raw.ID, result, err) + } + } + + return func(request []byte, raw *rawRequest) btcjson.Reply { + if chainSvr == nil { + err := btcjson.Error{ + Code: -1, + Message: "Chain server is disconnected", + } + return makeResponse(raw.ID, nil, err) + } + + res, err := chainSvr.RawRequest(raw.Method, raw.Params) + + // The raw result will only marshal correctly if called with the + // MarshalJSON method, and that method requires a pointer receiver. + return makeResponse(raw.ID, &res, err) + } +} + // ErrNoAuth represents an error where authentication could not succeed // due to a missing Authorization HTTP header. var ErrNoAuth = errors.New("no auth") @@ -466,6 +642,43 @@ func throttled(threshold int64, h http.Handler) http.Handler { }) } +type rawRequest struct { + // "jsonrpc" value isn't checked so we exclude it. + ID interface{} `json:"id"` + Method string `json:"method"` + Params []json.RawMessage `json:"params"` +} + +// idPointer returns a pointer to the passed ID, or nil if the interface is nil. +// Interface pointers are usually a red flag of doing something incorrectly, +// but this is only implemented here to work around an oddity with btcjson, +// which uses empty interface pointers for response IDs. +func idPointer(id interface{}) (p *interface{}) { + if id != nil { + p = &id + } + return +} + +// invalidAuth checks whether a websocket request is a valid (parsable) +// authenticate request and checks the supplied username and passphrase +// against the server auth. +func (s *rpcServer) invalidAuth(request []byte) bool { + cmd, err := btcjson.ParseMarshaledCmd(request) + if err != nil { + return false + } + authCmd, ok := cmd.(*btcws.AuthenticateCmd) + if !ok { + return false + } + // Check credentials. + login := authCmd.Username + ":" + authCmd.Passphrase + auth := "Basic " + base64.StdEncoding.EncodeToString([]byte(login)) + authSha := sha256.Sum256([]byte(auth)) + return subtle.ConstantTimeCompare(authSha[:], s.authsha[:]) != 1 +} + func (s *rpcServer) WebsocketClientRead(wsc *websocketClient) { for { _, request, err := wsc.conn.ReadMessage() @@ -481,113 +694,13 @@ func (s *rpcServer) WebsocketClientRead(wsc *websocketClient) { } } -type rawRequest struct { - // "jsonrpc" value isn't checked so we exclude it. - ID interface{} `json:"id"` - Method string `json:"method"` - Params []json.RawMessage `json:"params"` -} - -// idPointer returns a pointer to the passed ID, or nil if the interface is nil. -// Interface pointers are usually a red flag of doing something incorrectly, -// but this is only implemented here to work around an oddity with btcjson, -// which uses empty interface pointers for request and response IDs. -func idPointer(id interface{}) (p *interface{}) { - if id != nil { - p = &id - } - return -} - -func marshalError(id *interface{}) []byte { - response := btcjson.Reply{ - Id: id, - Error: &btcjson.ErrInvalidRequest, - } - mresponse, err := json.Marshal(response) - // We expect the marshal to succeed. If it doesn't, it indicates some - // non-marshalable type in the response. - if err != nil { - panic(err) - } - return mresponse -} - -// websocketPassthrough pass a websocket client's raw request to the connected -// chain server. -func (s *rpcServer) websocketPassthrough(wsc *websocketClient, request rawRequest) { - resp := passthrough(request) - _ = wsc.send(resp) -} - -// postPassthrough pass a websocket client's raw request to the connected -// chain server. -func (s *rpcServer) postPassthrough(w http.ResponseWriter, request rawRequest) { - resp := passthrough(request) - if _, err := w.Write(resp); err != nil { - log.Warnf("Unable to respond to client with passthrough "+ - "response: %v", err) - } -} - -// passthrough is a helper function for websocketPassthrough and postPassthrough -// to request and receive the chain server's marshaled response to an -// unhandled-by-wallet request. The marshaled response includes the original -// request's ID. -func passthrough(request rawRequest) []byte { - var res json.RawMessage - rpcc, err := accessClient() - if err == nil { - res, err = rpcc.RawRequest(request.Method, request.Params) - } - var jsonErr *btcjson.Error - if err != nil { - switch e := err.(type) { - case *btcjson.Error: - jsonErr = e - case btcjson.Error: - jsonErr = &e - default: - jsonErr = &btcjson.Error{ - Code: btcjson.ErrWallet.Code, - Message: err.Error(), - } - } - } - - // The raw result will only marshal correctly if called with the - // MarshalJSON method, and that method requires a pointer receiver. - var pres *json.RawMessage - if res != nil { - pres = &res - } - - resp := btcjson.Reply{ - Id: idPointer(request.ID), - Result: pres, - Error: jsonErr, - } - mresp, err := json.Marshal(resp) - // The chain server response was successfully unmarshaled or we created - // our own error, so a marshal can never error. - if err != nil { - panic(err) - } - return mresp -} - -type unauthedRequest struct { - marshaledRequest []byte - handler requestHandler -} - -func (s *rpcServer) WebsocketClientGateway(wsc *websocketClient) { -out: +func (s *rpcServer) WebsocketClientRespond(wsc *websocketClient) { // A for-select with a read of the quit channel is used instead of a // for-range to provide clean shutdown. This is necessary due to // WebsocketClientRead (which sends to the allRequests chan) not closing // allRequests during shutdown if the remote websocket client is still // connected. +out: for { select { case request, ok := <-wsc.allRequests: @@ -595,191 +708,136 @@ out: // client disconnected break out } - // Get the method of the request and check whether it - // should be handled by wallet or passed down to btcd. - // If the latter, handle in a new goroutine (to not - // block or be blocked by the handling of actual wallet - // requests). - // - // This is done by unmarshaling the JSON bytes into a - // rawRequest to avoid the mangling of unmarshaling and - // re-marshaling of large JSON numbers, as well as the - // overhead of unneeded unmarshals and marshals. + var raw rawRequest if err := json.Unmarshal(request, &raw); err != nil { if !wsc.authenticated { // Disconnect immediately. break out } - err = wsc.send(marshalError(idPointer(raw.ID))) + resp := makeResponse(raw.ID, nil, + btcjson.ErrInvalidRequest) + mresp, err := json.Marshal(resp) + // We expect the marshal to succeed. If it + // doesn't, it indicates some non-marshalable + // type in the response. + if err != nil { + panic(err) + } + err = wsc.send(mresp) if err != nil { break out } continue } - f, ok := handlerFunc(raw.Method, true) - if ok || raw.Method == "authenticate" { - // unauthedRequests is buffered to the max - // number of concurrent websocket client - // requests so as to not block the passthrough - // of later btcd requests. - wsc.unauthedRequests <- unauthedRequest{request, f} - } else { - // websocketPassthrough is run as a goroutine to - // send an unhandled request to the chain server - // without blocking the handling of later wallet - // requests. - go s.websocketPassthrough(wsc, raw) + switch raw.Method { + case "authenticate": + if wsc.authenticated || s.invalidAuth(request) { + // Disconnect immediately. + break out + } + wsc.authenticated = true + resp := makeResponse(raw.ID, nil, nil) + // Expected to never fail. + mresp, err := json.Marshal(resp) + if err != nil { + panic(err) + } + err = wsc.send(mresp) + if err != nil { + break out + } + + case "createencryptedwallet": + result, err := s.handleCreateEncryptedWallet(request) + resp := makeResponse(raw.ID, result, err) + mresp, err := json.Marshal(resp) + // Expected to never fail. + if err != nil { + panic(err) + } + err = wsc.send(mresp) + if err != nil { + break out + } + + case "stop": + s.Stop() + resp := makeResponse(raw.ID, + "btcwallet stopping.", nil) + mresp, err := json.Marshal(resp) + // Expected to never fail. + if err != nil { + panic(err) + } + err = wsc.send(mresp) + if err != nil { + break out + } + + default: + if !wsc.authenticated { + // Disconnect immediately. + break out + } + f := s.HandlerClosure(raw.Method) + s.wg.Add(1) + go func(request []byte, raw *rawRequest) { + resp := f(request, raw) + mresp, err := json.Marshal(resp) + if err != nil { + panic(err) + } + _ = wsc.send(mresp) + + s.wg.Done() + }(request, &raw) } + case <-s.quit: break out } } - close(wsc.unauthedRequests) - s.wg.Done() -} - -// invalidAuth checks whether a websocket request is allowed for the current -// authentication state. If an unauthenticated client submitted an -// authenticate request, the authentication is verified and the client's -// authentication state is modified. -func (s *rpcServer) invalidAuth(wsc *websocketClient, request btcjson.Cmd) (invalid, checked bool) { - if authCmd, ok := request.(*btcws.AuthenticateCmd); ok { - // Duplication authentication is not allowed. - if wsc.authenticated { - return true, false - } - - // Check credentials. - login := authCmd.Username + ":" + authCmd.Passphrase - auth := "Basic " + base64.StdEncoding.EncodeToString([]byte(login)) - authSha := sha256.Sum256([]byte(auth)) - cmp := subtle.ConstantTimeCompare(authSha[:], s.authsha[:]) - wsc.authenticated = cmp == 1 - return cmp != 1, true - } - // Unauthorized clients must first issue an authenticate request. If - // not already authenticated, the auth is invalid. - return !wsc.authenticated, false -} - -func (s *rpcServer) WebsocketClientRespond(wsc *websocketClient) { -out: - for r := range wsc.unauthedRequests { - cmd, parseErr := btcjson.ParseMarshaledCmd(r.marshaledRequest) - var id interface{} - if cmd != nil { - id = cmd.Id() - } - - // Verify that the websocket is authenticated and not send an - // unnecessary authentication request, or perform the check - // if unauthenticated and this is an authentication request. - // Disconnect the client immediately if the authentication is - // invalid or disallowed. - switch invalid, checked := s.invalidAuth(wsc, cmd); { - case invalid: - log.Warnf("Disconnecting improperly authenticated "+ - "websocket client %s", wsc.remoteAddr) - break out - case checked: - // Marshal and send a successful auth response. The - // marshal is expected to never fail. - response := btcjson.Reply{Id: idPointer(id)} - mresponse, err := json.Marshal(response) - if err != nil { - panic(err) - } - if err := wsc.send(mresponse); err != nil { - break out - } - continue - } - - // The parse error is checked after the authentication check - // so we don't respond back for invalid requests sent by - // unauthenticated clients. - if parseErr != nil { - if wsc.send(marshalError(idPointer(id))) != nil { - break out - } - continue - } - - // Send request and the handler func (already looked up) to the - // server's global request handler. This serializes the - // execution of all handlers from all connections (both - // websocket and HTTP POST), and runs the handler with exclusive - // access of the account manager. - var response handlerResponse - responseChan := make(chan handlerResponse) - job := handlerJob{ - request: cmd, - handler: r.handler, - response: responseChan, - } - select { - case s.requests <- job: - select { - case response = <-responseChan: - case <-s.quit: - break out - } - case <-s.quit: - break out - } - resp := btcjson.Reply{ - Id: idPointer(id), - Result: response.result, - Error: response.jsonErr, - } - mresp, err := json.Marshal(resp) - // All responses originating from us must be marshalable. - if err != nil { - panic(err) - } - // Send marshaled response to client. - if err := wsc.send(mresp); err != nil { - break out - } - } // Remove websocket client from notification group, or if the server is // shutting down, wait until the notification handler has finished // running. This is needed to ensure that no more notifications will be // sent to the client's responses chan before it's closed below. select { - case s.removeWSClient <- wsc: + case s.unregisterWSC <- wsc: case <-s.quit: <-s.notificationHandlerQuit } - close(wsc.responses) s.wg.Done() } func (s *rpcServer) WebsocketClientSend(wsc *websocketClient) { const deadline time.Duration = 2 * time.Second - for response := range wsc.responses { - err := wsc.conn.SetWriteDeadline(time.Now().Add(deadline)) - if err != nil { - log.Warnf("Cannot set write deadline on client %s: %v", - wsc.remoteAddr, err) - } - err = wsc.conn.WriteMessage(websocket.TextMessage, response) - if err != nil { - log.Warnf("Failed websocket send to client %s: %v", - wsc.remoteAddr, err) - break +out: + for { + select { + case response := <-wsc.responses: + err := wsc.conn.SetWriteDeadline(time.Now().Add(deadline)) + if err != nil { + log.Warnf("Cannot set write deadline on "+ + "client %s: %v", wsc.remoteAddr, err) + } + err = wsc.conn.WriteMessage(websocket.TextMessage, + response) + if err != nil { + log.Warnf("Failed websocket send to client "+ + "%s: %v", wsc.remoteAddr, err) + break out + } + + case <-s.quit: + break out } } close(wsc.quit) log.Infof("Disconnected websocket client %s", wsc.remoteAddr) - select { - case s.removeWSClient <- wsc: - case <-s.quit: - } s.wg.Done() } @@ -797,26 +855,28 @@ func (s *rpcServer) WebsocketClientRPC(wsc *websocketClient) { // Add client context so notifications duplicated to each // client are received by this client. select { - case s.addWSClient <- wsc: + case s.registerWSC <- wsc: case <-s.quit: return } + // TODO(jrick): this is crappy. kill it. + s.handlerLock.Lock() + if s.wallet != nil && s.chainSvr != nil { + s.wallet.notifyChainServerConnected(!s.chainSvr.Disconnected()) + } + s.handlerLock.Unlock() + // WebsocketClientRead is intentionally not run with the waitgroup // so it is ignored during shutdown. This is to prevent a hang during // shutdown where the goroutine is blocked on a read of the // websocket connection if the client is still connected. go s.WebsocketClientRead(wsc) - s.wg.Add(3) - go s.WebsocketClientGateway(wsc) + s.wg.Add(2) go s.WebsocketClientRespond(wsc) go s.WebsocketClientSend(wsc) - // Send initial unsolicited notifications. - // TODO: these should be requested by the client first. - s.NotifyConnectionStatus(wsc) - <-wsc.quit } @@ -841,62 +901,41 @@ func (s *rpcServer) PostClientRPC(w http.ResponseWriter, r *http.Request) { // requests, as they are invalid for HTTP POST clients. var raw rawRequest err = json.Unmarshal(rpcRequest, &raw) - if err != nil || raw.Method == "authenticate" { - _, err := w.Write(marshalError(idPointer(raw.ID))) + if err != nil { + resp := makeResponse(raw.ID, nil, btcjson.ErrInvalidRequest) + mresp, err := json.Marshal(resp) + // We expect the marshal to succeed. If it doesn't, it + // indicates some non-marshalable type in the response. + if err != nil { + panic(err) + } + _, err = w.Write(mresp) if err != nil { log.Warnf("Cannot write invalid request request to "+ "client: %v", err) } return } - f, ok := handlerFunc(raw.Method, false) - if !ok { - s.postPassthrough(w, raw) + + // Create the response and error from the request. Three special cases + // are handled for the authenticate, createencryptedwallet, and stop + // request methods. + var resp btcjson.Reply + switch raw.Method { + case "authenticate": + // Drop it. return + case "createencryptedwallet": + result, err := s.handleCreateEncryptedWallet(rpcRequest) + resp = makeResponse(raw.ID, result, err) + case "stop": + s.Stop() + resp = makeResponse(raw.ID, "btcwallet stopping.", nil) + default: + resp = s.HandlerClosure(raw.Method)(rpcRequest, &raw) } - // Parse the full request since it must be handled by wallet. - cmd, err := btcjson.ParseMarshaledCmd(rpcRequest) - var idPtr *interface{} - if cmd != nil { - idPtr = idPointer(cmd.Id()) - } - if err != nil { - _, err := w.Write(marshalError(idPtr)) - if err != nil { - log.Warnf("Client sent invalid request but unable "+ - "to respond with error: %v", err) - } - return - } - - // Send request and the handler func (already looked up) to the - // server's global request handler. This serializes the - // execution of all handlers from all connections (both - // websocket and HTTP POST), and runs the handler with exclusive - // access of the account manager. - var response handlerResponse - responseChan := make(chan handlerResponse) - job := handlerJob{ - request: cmd, - handler: f, - response: responseChan, - } - select { - case s.requests <- job: - select { - case response = <-responseChan: - case <-s.quit: - return - } - case <-s.quit: - return - } - resp := btcjson.Reply{ - Id: idPtr, - Result: response.result, - Error: response.jsonErr, - } + // Marshal and send. mresp, err := json.Marshal(resp) // All responses originating from us must be marshalable. if err != nil { @@ -908,46 +947,306 @@ func (s *rpcServer) PostClientRPC(w http.ResponseWriter, r *http.Request) { } } -// NotifyConnectionStatus notifies all connected websocket clients of the -// current connection status of btcwallet to btcd. -func (s *rpcServer) NotifyConnectionStatus(wsc *websocketClient) { - connected := false - rpcc, err := accessClient() - if err == nil { - connected = !rpcc.Disconnected() - } - ntfn := btcws.NewBtcdConnectedNtfn(connected) - mntfn, err := ntfn.MarshalJSON() - // btcws notifications must always marshal without error. - if err != nil { - panic(err) - } - if wsc == nil { - select { - case s.broadcasts <- mntfn: - case <-s.quit: - } - } else { - // Don't care whether the client disconnected at this - // point, so discard error. - _ = wsc.send(mntfn) +// Notification messages for websocket clients. +type ( + wsClientNotification interface { + // This returns a slice only because some of these types result + // in multpile client notifications. + notificationCmds(w *Wallet) []btcjson.Cmd } + + blockConnected keystore.BlockStamp + blockDisconnected keystore.BlockStamp + + txCredit txstore.Credit + txDebit txstore.Debits + + keystoreLocked bool + + confirmedBalance btcutil.Amount + unconfirmedBalance btcutil.Amount + + btcdConnected bool +) + +func (b blockConnected) notificationCmds(w *Wallet) []btcjson.Cmd { + n := btcws.NewBlockConnectedNtfn(b.Hash.String(), b.Height) + return []btcjson.Cmd{n} } -func (s *rpcServer) NotificationHandler() { +func (b blockDisconnected) notificationCmds(w *Wallet) []btcjson.Cmd { + n := btcws.NewBlockDisconnectedNtfn(b.Hash.String(), b.Height) + return []btcjson.Cmd{n} +} + +func (c txCredit) notificationCmds(w *Wallet) []btcjson.Cmd { + bs, err := w.chainSvr.BlockStamp() + if err != nil { + log.Warnf("Dropping tx credit notification due to unknown "+ + "chain height: %v", err) + return nil + } + ltr, err := txstore.Credit(c).ToJSON("", bs.Height, activeNet.Params) + if err != nil { + log.Errorf("Cannot create notification for transaction "+ + "credit: %v", err) + return nil + } + n := btcws.NewTxNtfn("", <r) + return []btcjson.Cmd{n} +} + +func (d txDebit) notificationCmds(w *Wallet) []btcjson.Cmd { + bs, err := w.chainSvr.BlockStamp() + if err != nil { + log.Warnf("Dropping tx debit notification due to unknown "+ + "chain height: %v", err) + return nil + } + ltrs, err := txstore.Debits(d).ToJSON("", bs.Height, activeNet.Params) + if err != nil { + log.Errorf("Cannot create notification for transaction "+ + "debits: %v", err) + return nil + } + ns := make([]btcjson.Cmd, len(ltrs)) + for i := range ns { + ns[i] = btcws.NewTxNtfn("", <rs[i]) + } + return ns +} + +func (kl keystoreLocked) notificationCmds(w *Wallet) []btcjson.Cmd { + n := btcws.NewWalletLockStateNtfn("", bool(kl)) + return []btcjson.Cmd{n} +} + +func (b confirmedBalance) notificationCmds(w *Wallet) []btcjson.Cmd { + n := btcws.NewAccountBalanceNtfn("", + btcutil.Amount(b).ToUnit(btcutil.AmountBTC), true) + return []btcjson.Cmd{n} +} + +func (b unconfirmedBalance) notificationCmds(w *Wallet) []btcjson.Cmd { + n := btcws.NewAccountBalanceNtfn("", + btcutil.Amount(b).ToUnit(btcutil.AmountBTC), false) + return []btcjson.Cmd{n} +} + +func (b btcdConnected) notificationCmds(w *Wallet) []btcjson.Cmd { + n := btcws.NewBtcdConnectedNtfn(bool(b)) + return []btcjson.Cmd{n} +} + +func (s *rpcServer) notificationListener() { out: for { select { - case c := <-s.addWSClient: - s.wsClients[c] = struct{}{} - case c := <-s.removeWSClient: - delete(s.wsClients, c) - case b := <-s.broadcasts: - for wsc := range s.wsClients { - if err := wsc.send(b); err != nil { - delete(s.wsClients, wsc) + case n := <-s.connectedBlocks: + s.enqueueNotification <- blockConnected(n) + case n := <-s.disconnectedBlocks: + s.enqueueNotification <- blockDisconnected(n) + case n := <-s.newCredits: + s.enqueueNotification <- txCredit(n) + case n := <-s.newDebits: + s.enqueueNotification <- txDebit(n) + case n := <-s.minedCredits: + s.enqueueNotification <- txCredit(n) + case n := <-s.minedDebits: + s.enqueueNotification <- txDebit(n) + case n := <-s.keystoreLocked: + s.enqueueNotification <- keystoreLocked(n) + case n := <-s.confirmedBalance: + s.enqueueNotification <- confirmedBalance(n) + case n := <-s.unconfirmedBalance: + s.enqueueNotification <- unconfirmedBalance(n) + case n := <-s.chainServerConnected: + s.enqueueNotification <- btcdConnected(n) + + // Registration of all notifications is done by the handler so + // it doesn't require another rpcServer mutex. + case <-s.registerWalletNtfns: + connectedBlocks, err := s.wallet.ListenConnectedBlocks() + if err != nil { + log.Errorf("Could not register for new "+ + "connected block notifications: %v", + err) + continue + } + disconnectedBlocks, err := s.wallet.ListenDisconnectedBlocks() + if err != nil { + log.Errorf("Could not register for new "+ + "disconnected block notifications: %v", + err) + continue + } + newCredits, err := s.wallet.TxStore.ListenNewCredits() + if err != nil { + log.Errorf("Could not register for new "+ + "credit notifications: %v", err) + continue + } + newDebits, err := s.wallet.TxStore.ListenNewDebits() + if err != nil { + log.Errorf("Could not register for new "+ + "debit notifications: %v", err) + continue + } + minedCredits, err := s.wallet.TxStore.ListenMinedCredits() + if err != nil { + log.Errorf("Could not register for mined "+ + "credit notifications: %v", err) + continue + } + minedDebits, err := s.wallet.TxStore.ListenMinedDebits() + if err != nil { + log.Errorf("Could not register for mined "+ + "debit notifications: %v", err) + continue + } + keystoreLocked, err := s.wallet.ListenKeystoreLockStatus() + if err != nil { + log.Errorf("Could not register for keystore "+ + "lock state changes: %v", err) + continue + } + confirmedBalance, err := s.wallet.ListenConfirmedBalance() + if err != nil { + log.Errorf("Could not register for confirmed "+ + "balance changes: %v", err) + continue + } + unconfirmedBalance, err := s.wallet.ListenUnconfirmedBalance() + if err != nil { + log.Errorf("Could not register for unconfirmed "+ + "balance changes: %v", err) + continue + } + chainServerConnected, err := s.wallet.ListenChainServerConnected() + if err != nil { + log.Errorf("Could not register for chain server "+ + "connection changes: %v", err) + continue + } + s.connectedBlocks = connectedBlocks + s.disconnectedBlocks = disconnectedBlocks + s.newCredits = newCredits + s.newDebits = newDebits + s.minedCredits = minedCredits + s.minedDebits = minedDebits + s.keystoreLocked = keystoreLocked + s.confirmedBalance = confirmedBalance + s.unconfirmedBalance = unconfirmedBalance + s.chainServerConnected = chainServerConnected + + case <-s.quit: + break out + } + } + close(s.enqueueNotification) + go s.drainNotifications() + s.wg.Done() +} + +func (s *rpcServer) drainNotifications() { + for { + select { + case <-s.connectedBlocks: + case <-s.disconnectedBlocks: + case <-s.newCredits: + case <-s.newDebits: + case <-s.minedCredits: + case <-s.minedDebits: + case <-s.confirmedBalance: + case <-s.unconfirmedBalance: + } + } +} + +// notifiationQueue manages a queue of empty interfaces, reading from in and +// sending the oldest unsent to out. This handler stops when either of the +// in or quit channels are closed, and closes out before returning, without +// waiting to send any variables still remaining in the queue. +func (s *rpcServer) notificationQueue() { + var q []wsClientNotification + var dequeue chan<- wsClientNotification + skipQueue := s.dequeueNotification + var next wsClientNotification +out: + for { + select { + case n, ok := <-s.enqueueNotification: + if !ok { + // Sender closed input channel. + break out + } + + // Either send to out immediately if skipQueue is + // non-nil (queue is empty) and reader is ready, + // or append to the queue and send later. + select { + case skipQueue <- n: + default: + q = append(q, n) + dequeue = s.dequeueNotification + skipQueue = nil + next = q[0] + } + + case dequeue <- next: + q[0] = nil // avoid leak + q = q[1:] + if len(q) == 0 { + dequeue = nil + skipQueue = s.dequeueNotification + } + + case <-s.quit: + break out + } + } + close(s.dequeueNotification) + s.wg.Done() +} + +func (s *rpcServer) notificationHandler() { + clients := make(map[chan struct{}]*websocketClient) +out: + for { + select { + case c := <-s.registerWSC: + clients[c.quit] = c + + case c := <-s.unregisterWSC: + delete(clients, c.quit) + + case nmsg, ok := <-s.dequeueNotification: + // No more notifications. + if !ok { + break out + } + + // Ignore if there are no clients to receive the + // notification. + if len(clients) == 0 { + continue + } + + ns := nmsg.notificationCmds(s.wallet) + for _, n := range ns { + mn, err := n.MarshalJSON() + // All notifications are expected to be + // marshalable. + if err != nil { + panic(err) + } + for _, c := range clients { + if err := c.send(mn); err != nil { + delete(clients, c.quit) + } } } + case <-s.quit: break out } @@ -961,10 +1260,10 @@ out: // or any of the above special error classes, the server will respond with // the JSON-RPC appropiate error code. All other errors use the wallet // catch-all error code, btcjson.ErrWallet.Code. -type requestHandler func(btcjson.Cmd) (interface{}, error) +type requestHandler func(*Wallet, *chain.Client, btcjson.Cmd) (interface{}, error) var rpcHandlers = map[string]requestHandler{ - // Standard bitcoind methods (implemented) + // Reference implementation wallet methods (implemented) "addmultisigaddress": AddMultiSigAddress, "createmultisig": CreateMultiSig, "dumpprivkey": DumpPrivKey, @@ -992,14 +1291,13 @@ var rpcHandlers = map[string]requestHandler{ "settxfee": SetTxFee, "signmessage": SignMessage, "signrawtransaction": SignRawTransaction, - "stop": Stop, "validateaddress": ValidateAddress, "verifymessage": VerifyMessage, "walletlock": WalletLock, "walletpassphrase": WalletPassphrase, "walletpassphrasechange": WalletPassphraseChange, - // Standard bitcoind methods (currently unimplemented) + // Reference implementation methods (still unimplemented) "backupwallet": Unimplemented, "dumpwallet": Unimplemented, "getreceivedbyaddress": Unimplemented, @@ -1010,17 +1308,17 @@ var rpcHandlers = map[string]requestHandler{ "move": Unimplemented, "setaccount": Unimplemented, - // Standard bitcoind methods which won't be implemented by btcwallet. + // Reference methods which can't be implemented by btcwallet due to + // design decision differences "encryptwallet": Unsupported, - // Extensions not exclusive to websocket connections. - "createencryptedwallet": CreateEncryptedWallet, -} - -// Extensions exclusive to websocket connections. -var wsHandlers = map[string]requestHandler{ - "exportwatchingwallet": ExportWatchingWallet, - "getaddressbalance": GetAddressBalance, + // Extensions to the reference client JSON-RPC API + "exportwatchingwallet": ExportWatchingWallet, + "getaddressbalance": GetAddressBalance, + // This was an extension but the reference implementation added it as + // well, but with a different API (no account parameter). It's listed + // here because it hasn't been update to use the reference + // implemenation's API. "getunconfirmedbalance": GetUnconfirmedBalance, "listaddresstransactions": ListAddressTransactions, "listalltransactions": ListAllTransactions, @@ -1028,96 +1326,132 @@ var wsHandlers = map[string]requestHandler{ "walletislocked": WalletIsLocked, } -// handlerFunc looks up a request handler func for the passed method from -// the http post and (if the request is from a websocket connection) websocket -// handler maps. If a suitable handler could not be found, ok is false. -func handlerFunc(method string, ws bool) (f requestHandler, ok bool) { - f, ok = rpcHandlers[method] - if !ok && ws { - f, ok = wsHandlers[method] - } - return f, ok -} - -type handlerResponse struct { - result interface{} - jsonErr *btcjson.Error -} - -type handlerJob struct { - request btcjson.Cmd - handler requestHandler - response chan<- handlerResponse -} - -// RequestHandler reads and processes client requests from the request channel. -// Each request is run with exclusive access to the account manager. -func (s *rpcServer) RequestHandler() { -out: - for { - select { - case r := <-s.requests: - AcctMgr.Grab() - result, err := r.handler(r.request) - AcctMgr.Release() - - var jsonErr *btcjson.Error - if err != nil { - jsonErr = &btcjson.Error{Message: err.Error()} - switch e := err.(type) { - case btcjson.Error: - *jsonErr = e - case *btcjson.Error: - *jsonErr = *e - case DeserializationError: - jsonErr.Code = btcjson.ErrDeserialization.Code - case InvalidParameterError: - jsonErr.Code = btcjson.ErrInvalidParameter.Code - case ParseError: - jsonErr.Code = btcjson.ErrParse.Code - case InvalidAddressOrKeyError: - jsonErr.Code = btcjson.ErrInvalidAddressOrKey.Code - default: // All other errors get the wallet error code. - jsonErr.Code = btcjson.ErrWallet.Code - } - } - // The goroutine which requested this may not be running - // anymore. If the quit chan is read instead, break out - // of the loop now so more requests aren't potentially - // read after reentering the loop. - select { - case r.response <- handlerResponse{result, jsonErr}: - case <-s.quit: - break out - } - - case <-s.quit: - break out - } - } - s.wg.Done() -} - // Unimplemented handles an unimplemented RPC request with the // appropiate error. -func Unimplemented(btcjson.Cmd) (interface{}, error) { +func Unimplemented(*Wallet, *chain.Client, btcjson.Cmd) (interface{}, error) { return nil, btcjson.ErrUnimplemented } // Unsupported handles a standard bitcoind RPC request which is // unsupported by btcwallet due to design differences. -func Unsupported(btcjson.Cmd) (interface{}, error) { +func Unsupported(*Wallet, *chain.Client, btcjson.Cmd) (interface{}, error) { return nil, btcjson.Error{ Code: -1, Message: "Request unsupported by btcwallet", } } +// UnloadedWallet is the handler func that is run when a wallet has not been +// loaded yet when trying to execute a wallet RPC. +func UnloadedWallet(*Wallet, *chain.Client, btcjson.Cmd) (interface{}, error) { + return nil, ErrUnloadedWallet +} + +// NoEncryptedWallet is the handler func that is run when no wallet has been +// created by the user yet. +// loaded yet when trying to execute a wallet RPC. +func NoEncryptedWallet(*Wallet, *chain.Client, btcjson.Cmd) (interface{}, error) { + return nil, btcjson.Error{ + Code: btcjson.ErrWallet.Code, + Message: "Request requires a wallet but no wallet has been " + + "created -- use createencryptedwallet to recover", + } +} + +// TODO(jrick): may be a good idea to add handlers for passthrough to the chain +// server. If a handler can not be looked up in one of the above maps, use this +// passthrough handler instead. This isn't done at the moment since all +// requests are executed serialized, and blocking all requests, and even just +// requests from the same client, on the result of a btcd RPC can result is too +// much waiting for the round trip. + +// lookupAnyHandler looks up a request handler func for the passed method from +// the http post and (if the request is from a websocket connection) websocket +// handler maps. If a suitable handler could not be found, ok is false. +func lookupAnyHandler(method string) (f requestHandler, ok bool) { + f, ok = rpcHandlers[method] + return +} + +// unloadedWalletHandlerFunc looks up whether a request requires a wallet, and +// if so, returns a specialized handler func to return errors for an unloaded +// wallet component necessary to complete the request. If ok is false, the +// function is invalid and should be passed through instead. +func unloadedWalletHandlerFunc(method string) (f requestHandler, ok bool) { + _, ok = rpcHandlers[method] + if ok { + f = UnloadedWallet + } + return +} + +// missingWalletHandlerFunc looks up whether a request requires a wallet, and +// if so, returns a specialized handler func to return errors for no wallets +// being created yet with the createencryptedwallet RPC. If ok is false, the +// function is invalid and should be passed through instead. +func missingWalletHandlerFunc(method string) (f requestHandler, ok bool) { + _, ok = rpcHandlers[method] + if ok { + f = NoEncryptedWallet + } + return +} + +// requestHandlerClosure is a closure over a requestHandler or passthrough +// request with the RPC server's wallet and chain server variables as part +// of the closure context. +type requestHandlerClosure func([]byte, *rawRequest) btcjson.Reply + +// makeResponse makes the JSON-RPC response struct for the result and error +// returned by a requestHandler. The returned response is not ready for +// marshaling and sending off to a client, but must be +func makeResponse(id, result interface{}, err error) btcjson.Reply { + idPtr := idPointer(id) + if err != nil { + return btcjson.Reply{ + Id: idPtr, + Error: jsonError(err), + } + } + return btcjson.Reply{ + Id: idPtr, + Result: result, + } +} + +// jsonError creates a JSON-RPC error from the Go error. +func jsonError(err error) *btcjson.Error { + if err == nil { + return nil + } + + jsonErr := btcjson.Error{ + Message: err.Error(), + } + switch e := err.(type) { + case btcjson.Error: + return &e + case *btcjson.Error: + return e + case DeserializationError: + jsonErr.Code = btcjson.ErrDeserialization.Code + case InvalidParameterError: + jsonErr.Code = btcjson.ErrInvalidParameter.Code + case ParseError: + jsonErr.Code = btcjson.ErrParse.Code + case InvalidAddressOrKeyError: + jsonErr.Code = btcjson.ErrInvalidAddressOrKey.Code + default: // All other errors get the wallet error code. + jsonErr.Code = btcjson.ErrWallet.Code + } + return &jsonErr +} + // makeMultiSigScript is a helper function to combine common logic for // AddMultiSig and CreateMultiSig. // all error codes are rpc parse error here to match bitcoind which just throws // a runtime exception. *sigh*. -func makeMultiSigScript(keys []string, nRequired int) ([]byte, error) { +func makeMultiSigScript(w *Wallet, keys []string, nRequired int) ([]byte, error) { keysesPrecious := make([]*btcutil.AddressPubKey, len(keys)) // The address list will made up either of addreseses (pubkey hash), for @@ -1134,7 +1468,7 @@ func makeMultiSigScript(keys []string, nRequired int) ([]byte, error) { case *btcutil.AddressPubKey: keysesPrecious[i] = addr case *btcutil.AddressPubKeyHash: - ainfo, err := AcctMgr.Address(addr) + ainfo, err := w.KeyStore.Address(addr) if err != nil { return nil, err } @@ -1160,53 +1494,42 @@ func makeMultiSigScript(keys []string, nRequired int) ([]byte, error) { // AddMultiSigAddress handles an addmultisigaddress request by adding a // multisig address to the given wallet. -func AddMultiSigAddress(icmd btcjson.Cmd) (interface{}, error) { - cmd, ok := icmd.(*btcjson.AddMultisigAddressCmd) - if !ok { - return nil, btcjson.ErrInternal - } +func AddMultiSigAddress(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) { + cmd := icmd.(*btcjson.AddMultisigAddressCmd) - acct, err := AcctMgr.Account(cmd.Account) + err := checkDefaultAccount(cmd.Account) if err != nil { - if err == ErrNotFound { - return nil, btcjson.ErrWalletInvalidAccountName - } return nil, err } - script, err := makeMultiSigScript(cmd.Keys, cmd.NRequired) + script, err := makeMultiSigScript(w, cmd.Keys, cmd.NRequired) if err != nil { return nil, ParseError{err} } // TODO(oga) blockstamp current block? - address, err := acct.KeyStore.ImportScript(script, + address, err := w.KeyStore.ImportScript(script, &keystore.BlockStamp{}) if err != nil { return nil, err } // Write wallet with imported multisig address to disk. - AcctMgr.ds.ScheduleWalletWrite(acct) - if err := AcctMgr.ds.FlushAccount(acct); err != nil { + w.KeyStore.MarkDirty() + err = w.KeyStore.WriteIfDirty() + if err != nil { return nil, fmt.Errorf("account write failed: %v", err) } - // Associate the import address with this account. - AcctMgr.MarkAddressForAccount(address, acct) - return address.EncodeAddress(), nil } // CreateMultiSig handles an createmultisig request by returning a // multisig address for the given inputs. -func CreateMultiSig(icmd btcjson.Cmd) (interface{}, error) { - cmd, ok := icmd.(*btcjson.CreateMultisigCmd) - if !ok { - return nil, btcjson.ErrInternal - } +func CreateMultiSig(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) { + cmd := icmd.(*btcjson.CreateMultisigCmd) - script, err := makeMultiSigScript(cmd.Keys, cmd.NRequired) + script, err := makeMultiSigScript(w, cmd.Keys, cmd.NRequired) if err != nil { return nil, ParseError{err} } @@ -1226,19 +1549,15 @@ func CreateMultiSig(icmd btcjson.Cmd) (interface{}, error) { // DumpPrivKey handles a dumpprivkey request with the private key // for a single address, or an appropiate error if the wallet // is locked. -func DumpPrivKey(icmd btcjson.Cmd) (interface{}, error) { - // Type assert icmd to access parameters. - cmd, ok := icmd.(*btcjson.DumpPrivKeyCmd) - if !ok { - return nil, btcjson.ErrInternal - } +func DumpPrivKey(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) { + cmd := icmd.(*btcjson.DumpPrivKeyCmd) addr, err := btcutil.DecodeAddress(cmd.Address, activeNet.Params) if err != nil { return nil, btcjson.ErrInvalidAddressOrKey } - key, err := AcctMgr.DumpWIFPrivateKey(addr) + key, err := w.DumpWIFPrivateKey(addr) if err == keystore.ErrLocked { // Address was found, but the private key isn't // accessible. @@ -1250,17 +1569,9 @@ func DumpPrivKey(icmd btcjson.Cmd) (interface{}, error) { // DumpWallet handles a dumpwallet request by returning all private // keys in a wallet, or an appropiate error if the wallet is locked. // TODO: finish this to match bitcoind by writing the dump to a file. -func DumpWallet(icmd btcjson.Cmd) (interface{}, error) { - // Type assert icmd to access parameters. - _, ok := icmd.(*btcjson.DumpWalletCmd) - if !ok { - return nil, btcjson.ErrInternal - } - - keys, err := AcctMgr.DumpKeys() +func DumpWallet(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) { + keys, err := w.DumpPrivKeys() if err == keystore.ErrLocked { - // Address was found, but the private key isn't - // accessible. return nil, btcjson.ErrWalletUnlockNeeded } return keys, err @@ -1268,109 +1579,80 @@ func DumpWallet(icmd btcjson.Cmd) (interface{}, 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{}, error) { - // Type assert icmd to access parameters. - cmd, ok := icmd.(*btcws.ExportWatchingWalletCmd) - if !ok { - return nil, btcjson.ErrInternal - } +// returning base64-encoding of serialized account files. +// +// TODO: remove Download from the command, this always assumes download now. +func ExportWatchingWallet(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) { + cmd := icmd.(*btcws.ExportWatchingWalletCmd) - a, err := AcctMgr.Account(cmd.Account) - if err != nil { - if err == ErrNotFound { - return nil, btcjson.ErrWalletInvalidAccountName - } - return nil, err - } - - wa, err := a.ExportWatchingWallet() + err := checkAccountName(cmd.Account) if err != nil { return nil, err } - if cmd.Download { - return wa.exportBase64() + wa, err := w.ExportWatchingWallet() + if err != nil { + return nil, err } - // Create export directory, write files there. - err = wa.ExportToDirectory("watchingwallet") - return nil, err + return wa.exportBase64() } // GetAddressesByAccount handles a getaddressesbyaccount request by returning // all addresses for an account, or an error if the requested account does // not exist. -func GetAddressesByAccount(icmd btcjson.Cmd) (interface{}, error) { - // Type assert icmd to access parameters. - cmd, ok := icmd.(*btcjson.GetAddressesByAccountCmd) - if !ok { - return nil, btcjson.ErrInternal - } +func GetAddressesByAccount(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) { + cmd := icmd.(*btcjson.GetAddressesByAccountCmd) - a, err := AcctMgr.Account(cmd.Account) + err := checkAccountName(cmd.Account) if err != nil { - if err == ErrNotFound { - return nil, btcjson.ErrWalletInvalidAccountName - } return nil, err } - return a.SortedActivePaymentAddresses(), nil + + return w.SortedActivePaymentAddresses(), nil } // GetBalance handles a getbalance request by returning the balance for an // account (wallet), or an error if the requested account does not // exist. -func GetBalance(icmd btcjson.Cmd) (interface{}, error) { - // Type assert icmd to access parameters. - cmd, ok := icmd.(*btcjson.GetBalanceCmd) - if !ok { - return nil, btcjson.ErrInternal +func GetBalance(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) { + cmd := icmd.(*btcjson.GetBalanceCmd) + + err := checkAccountName(cmd.Account) + if err != nil { + return nil, err } - balance, err := AcctMgr.CalculateBalance(cmd.Account, cmd.MinConf) - if err == ErrNotFound { - return nil, btcjson.ErrWalletInvalidAccountName + balance, err := w.CalculateBalance(cmd.MinConf) + if err != nil { + return nil, err } - return balance.ToUnit(btcutil.AmountBTC), err + + return balance.ToUnit(btcutil.AmountBTC), nil } // GetInfo handles a getinfo request by returning the a structure containing // information about the current state of btcwallet. // exist. -func GetInfo(icmd btcjson.Cmd) (interface{}, error) { +func GetInfo(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) { // Call down to btcd for all of the information in this command known // by them. - rpcc, err := accessClient() - if err != nil { - return nil, err - } - info, err := rpcc.GetInfo() + info, err := chainSvr.GetInfo() if err != nil { return nil, err } - var balance, feeIncr btcutil.Amount - accounts := AcctMgr.AllAccounts() - for _, a := range accounts { - bal, err := a.CalculateBalance(1) - if err == nil { - balance += bal - } - // For now we assume all transactions can only be created - // with the default account (as this is the only account - // that's usable), so use it for the fee increment. - if a.name == "" { - feeIncr = a.FeeIncrement - } + bal, err := w.CalculateBalance(1) + if err != nil { + return nil, err } + info.WalletVersion = int32(keystore.VersCurrent.Uint32()) - info.Balance = balance.ToUnit(btcutil.AmountBTC) + info.Balance = bal.ToUnit(btcutil.AmountBTC) // Keypool times are not tracked. set to current time. info.KeypoolOldest = time.Now().Unix() info.KeypoolSize = int32(cfg.KeypoolSize) - info.PaytxFee = feeIncr.ToUnit(btcutil.AmountBTC) + info.PaytxFee = w.FeeIncrement.ToUnit(btcutil.AmountBTC) // We don't set the following since they don't make much sense in the // wallet architecture: // - unlocked_until @@ -1381,12 +1663,8 @@ func GetInfo(icmd btcjson.Cmd) (interface{}, error) { // GetAccount handles a getaccount request by returning the account name // associated with a single address. -func GetAccount(icmd btcjson.Cmd) (interface{}, error) { - // Type assert icmd to access parameters. - cmd, ok := icmd.(*btcjson.GetAccountCmd) - if !ok { - return nil, btcjson.ErrInternal - } +func GetAccount(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) { + cmd := icmd.(*btcjson.GetAccountCmd) // Is address valid? addr, err := btcutil.DecodeAddress(cmd.Address, activeNet.Params) @@ -1394,15 +1672,13 @@ func GetAccount(icmd btcjson.Cmd) (interface{}, error) { return nil, btcjson.ErrInvalidAddressOrKey } - // Look up account which holds this address. - acct, err := AcctMgr.AccountByAddress(addr) + // If it is in the wallet, we consider it part of the default account. + _, err = w.KeyStore.Address(addr) if err != nil { - if err == ErrNotFound { - return nil, ErrAddressNotInWallet - } - return nil, err + return nil, btcjson.ErrInvalidAddressOrKey } - return acct.KeyStore.Name(), nil + + return "", nil } // GetAccountAddress handles a getaccountaddress by returning the most @@ -1411,41 +1687,27 @@ func GetAccount(icmd btcjson.Cmd) (interface{}, error) { // If the most recently-requested address has been used, a new address (the // next chained address in the keypool) is used. This can fail if the keypool // runs out (and will return btcjson.ErrWalletKeypoolRanOut if that happens). -func GetAccountAddress(icmd btcjson.Cmd) (interface{}, error) { - // Type assert icmd to access parameters. - cmd, ok := icmd.(*btcjson.GetAccountAddressCmd) - if !ok { - return nil, btcjson.ErrInternal - } +func GetAccountAddress(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) { + cmd := icmd.(*btcjson.GetAccountAddressCmd) - // Lookup account for this request. - a, err := AcctMgr.Account(cmd.Account) + err := checkDefaultAccount(cmd.Account) if err != nil { - if err == ErrNotFound { - return nil, btcjson.ErrWalletInvalidAccountName - } return nil, err } - addr, err := a.CurrentAddress() + addr, err := w.CurrentAddress() if err != nil { - if err == keystore.ErrLocked { - return nil, btcjson.ErrWalletKeypoolRanOut - } return nil, err } + return addr.EncodeAddress(), err } // GetAddressBalance handles a getaddressbalance extension request by // returning the current balance (sum of unspent transaction output amounts) // for a single address. -func GetAddressBalance(icmd btcjson.Cmd) (interface{}, error) { - // Type assert icmd to access parameters. - cmd, ok := icmd.(*btcws.GetAddressBalanceCmd) - if !ok { - return nil, btcjson.ErrInternal - } +func GetAddressBalance(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) { + cmd := icmd.(*btcws.GetAddressBalanceCmd) // Is address valid? addr, err := btcutil.DecodeAddress(cmd.Address, activeNet.Params) @@ -1453,39 +1715,35 @@ func GetAddressBalance(icmd btcjson.Cmd) (interface{}, error) { return nil, btcjson.ErrInvalidAddressOrKey } - // Get the account which holds the address in the request. - a, err := AcctMgr.AccountByAddress(addr) + // Check if address is managed by this wallet. + _, err = w.KeyStore.Address(addr) if err != nil { return nil, ErrAddressNotInWallet } - bal, err := a.CalculateAddressBalance(addr, int(cmd.Minconf)) - return bal.ToUnit(btcutil.AmountBTC), err + bal, err := w.CalculateAddressBalance(addr, int(cmd.Minconf)) + if err != nil { + return nil, err + } + + return bal.ToUnit(btcutil.AmountBTC), nil } // GetUnconfirmedBalance handles a getunconfirmedbalance extension request // by returning the current unconfirmed balance of an account. -func GetUnconfirmedBalance(icmd btcjson.Cmd) (interface{}, error) { - // Type assert icmd to access parameters. - cmd, ok := icmd.(*btcws.GetUnconfirmedBalanceCmd) - if !ok { - return nil, btcjson.ErrInternal - } +func GetUnconfirmedBalance(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) { + cmd := icmd.(*btcws.GetUnconfirmedBalanceCmd) - // Get the account included in the request. - a, err := AcctMgr.Account(cmd.Account) - if err != nil { - if err == ErrNotFound { - return nil, btcjson.ErrWalletInvalidAccountName - } - return nil, err - } - - unconfirmed, err := a.CalculateBalance(0) + err := checkAccountName(cmd.Account) if err != nil { return nil, err } - confirmed, err := a.CalculateBalance(1) + + unconfirmed, err := w.CalculateBalance(0) + if err != nil { + return nil, err + } + confirmed, err := w.CalculateBalance(1) if err != nil { return nil, err } @@ -1495,20 +1753,12 @@ func GetUnconfirmedBalance(icmd btcjson.Cmd) (interface{}, error) { // ImportPrivKey handles an importprivkey request by parsing // a WIF-encoded private key and adding it to an account. -func ImportPrivKey(icmd btcjson.Cmd) (interface{}, error) { - // Type assert icmd to access parameters. - cmd, ok := icmd.(*btcjson.ImportPrivKeyCmd) - if !ok { - return nil, btcjson.ErrInternal - } +func ImportPrivKey(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) { + cmd := icmd.(*btcjson.ImportPrivKeyCmd) - // Get the acount included in the request. Yes, Label is the - // account name... - a, err := AcctMgr.Account(cmd.Label) + // Yes, Label is the account name... + err := checkDefaultAccount(cmd.Label) if err != nil { - if err == ErrNotFound { - return nil, btcjson.ErrWalletInvalidAccountName - } return nil, err } @@ -1518,86 +1768,37 @@ func ImportPrivKey(icmd btcjson.Cmd) (interface{}, error) { } // Import the private key, handling any errors. - bs := keystore.BlockStamp{} - if _, err := a.ImportPrivateKey(wif, &bs, cmd.Rescan); err != nil { - switch err { - case keystore.ErrDuplicate: - // Do not return duplicate key errors to the client. - return nil, nil - case keystore.ErrLocked: - return nil, btcjson.ErrWalletUnlockNeeded - default: - return nil, err - } + _, err = w.ImportPrivateKey(wif, &keystore.BlockStamp{}, cmd.Rescan) + switch err { + case keystore.ErrDuplicate: + // Do not return duplicate key errors to the client. + return nil, nil + case keystore.ErrLocked: + return nil, btcjson.ErrWalletUnlockNeeded + default: + // If the import was successful, reply with nil. + return nil, err } - - // If the import was successful, reply with nil. - return nil, nil } // KeypoolRefill handles the keypoolrefill command. Since we handle the keypool // automatically this does nothing since refilling is never manually required. -func KeypoolRefill(icmd btcjson.Cmd) (interface{}, error) { +func KeypoolRefill(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) { return nil, nil } -// NotifyNewBlockChainHeight notifies all websocket clients of a new -// blockchain height. This sends the same notification as -// btcd, so this can probably be removed. -func (s *rpcServer) NotifyNewBlockChainHeight(bs *keystore.BlockStamp) { - ntfn := btcws.NewBlockConnectedNtfn(bs.Hash.String(), bs.Height) - mntfn, err := ntfn.MarshalJSON() - // btcws notifications must always marshal without error. - if err != nil { - panic(err) - } - select { - case s.broadcasts <- mntfn: - case <-s.quit: - } -} - -// NotifyBalances notifies an attached websocket clients of the current -// confirmed and unconfirmed account balances. -// -// TODO(jrick): Switch this to return a single JSON object -// (map[string]interface{}) of all accounts and their balances, instead of -// separate notifications for each account. -func (s *rpcServer) NotifyBalances() { - for _, a := range AcctMgr.AllAccounts() { - balance, err := a.CalculateBalance(1) - var unconfirmed btcutil.Amount - if err == nil { - unconfirmed, err = a.CalculateBalance(0) - } - if err != nil { - break - } - unconfirmed -= balance - s.NotifyWalletBalance(a.name, balance) - s.NotifyWalletBalanceUnconfirmed(a.name, unconfirmed) - } -} - // GetNewAddress handlesa getnewaddress request by returning a new // address for an account. If the account does not exist or the keypool // ran out with a locked wallet, an appropiate error is returned. -func GetNewAddress(icmd btcjson.Cmd) (interface{}, error) { - // Type assert icmd to access parameters. - cmd, ok := icmd.(*btcjson.GetNewAddressCmd) - if !ok { - return nil, btcjson.ErrInternal - } +func GetNewAddress(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) { + cmd := icmd.(*btcjson.GetNewAddressCmd) - a, err := AcctMgr.Account(cmd.Account) + err := checkDefaultAccount(cmd.Account) if err != nil { - if err == ErrNotFound { - return nil, btcjson.ErrWalletInvalidAccountName - } return nil, err } - addr, err := a.NewAddress() + addr, err := w.NewAddress() if err != nil { return nil, err } @@ -1611,21 +1812,8 @@ func GetNewAddress(icmd btcjson.Cmd) (interface{}, error) { // // Note: bitcoind allows specifying the account as an optional parameter, // but ignores the parameter. -func GetRawChangeAddress(icmd btcjson.Cmd) (interface{}, error) { - cmd, ok := icmd.(*btcjson.GetRawChangeAddressCmd) - if !ok { - return nil, btcjson.ErrInternal - } - - a, err := AcctMgr.Account(cmd.Account) - if err != nil { - if err == ErrNotFound { - return nil, btcjson.ErrWalletInvalidAccountName - } - return nil, err - } - - addr, err := a.NewChangeAddress() +func GetRawChangeAddress(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) { + addr, err := w.NewChangeAddress() if err != nil { return nil, err } @@ -1636,179 +1824,146 @@ func GetRawChangeAddress(icmd btcjson.Cmd) (interface{}, error) { // GetReceivedByAccount handles a getreceivedbyaccount request by returning // the total amount received by addresses of an account. -func GetReceivedByAccount(icmd btcjson.Cmd) (interface{}, error) { - cmd, ok := icmd.(*btcjson.GetReceivedByAccountCmd) - if !ok { - return nil, btcjson.ErrInternal - } +func GetReceivedByAccount(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) { + cmd := icmd.(*btcjson.GetReceivedByAccountCmd) - a, err := AcctMgr.Account(cmd.Account) + err := checkAccountName(cmd.Account) if err != nil { - if err == ErrNotFound { - return nil, btcjson.ErrWalletInvalidAccountName - } return nil, err } - bal, err := a.TotalReceived(cmd.MinConf) - return bal.ToUnit(btcutil.AmountBTC), err + bal, err := w.TotalReceived(cmd.MinConf) + if err != nil { + return nil, err + } + + return bal.ToUnit(btcutil.AmountBTC), nil } // GetTransaction handles a gettransaction request by returning details about // a single transaction saved by wallet. -func GetTransaction(icmd btcjson.Cmd) (interface{}, error) { - // Type assert icmd to access parameters. - cmd, ok := icmd.(*btcjson.GetTransactionCmd) - if !ok { - return nil, btcjson.ErrInternal - } +func GetTransaction(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) { + cmd := icmd.(*btcjson.GetTransactionCmd) txSha, err := btcwire.NewShaHashFromStr(cmd.Txid) if err != nil { return nil, btcjson.ErrDecodeHexString } - accumulatedTxen := AcctMgr.GetTransaction(txSha) - if len(accumulatedTxen) == 0 { + record, ok := w.TxRecord(txSha) + if !ok { return nil, btcjson.ErrNoTxInfo } - rpcc, err := accessClient() - if err != nil { - return nil, err - } - bs, err := rpcc.BlockStamp() + bs, err := w.SyncedChainTip() if err != nil { return nil, err } - received := btcutil.Amount(0) - var debits *txstore.Debits - var debitAccount string - var targetAddr string + var txBuf bytes.Buffer + txBuf.Grow(record.Tx().MsgTx().SerializeSize()) + err = record.Tx().MsgTx().Serialize(&txBuf) + if err != nil { + return nil, err + } + // TODO(jrick) set "generate" to true if this is the coinbase (if + // record.Tx().Index() == 0). ret := btcjson.GetTransactionResult{ - Details: []btcjson.GetTransactionDetailsResult{}, + TxID: txSha.String(), + Hex: hex.EncodeToString(txBuf.Bytes()), + Time: record.Received().Unix(), + TimeReceived: record.Received().Unix(), WalletConflicts: []string{}, } - details := []btcjson.GetTransactionDetailsResult{} - for _, e := range accumulatedTxen { - for _, cred := range e.Tx.Credits() { - // Change is ignored. - if cred.Change() { - continue - } - received += cred.Amount() - - var addr string - // Errors don't matter here, as we only consider the - // case where len(addrs) == 1. - _, addrs, _, _ := cred.Addresses(activeNet.Params) - if len(addrs) == 1 { - addr = addrs[0].EncodeAddress() - // The first non-change output address is considered the - // target for sent transactions. - if targetAddr == "" { - targetAddr = addr - } - } - - details = append(details, btcjson.GetTransactionDetailsResult{ - Account: e.Account, - Category: cred.Category(bs.Height).String(), - Amount: cred.Amount().ToUnit(btcutil.AmountBTC), - Address: addr, - }) - } - - if d, err := e.Tx.Debits(); err == nil { - // There should only be a single debits record for any - // of the account's transaction records. - debits = &d - debitAccount = e.Account + if record.BlockHeight != -1 { + txBlock, err := record.Block() + if err != nil { + return nil, err } + ret.BlockIndex = int64(record.Tx().Index()) + ret.BlockHash = txBlock.Hash.String() + ret.BlockTime = txBlock.Time.Unix() + ret.Confirmations = int64(record.Confirmations(bs.Height)) } - totalAmount := received - if debits != nil { - totalAmount -= debits.InputAmount() - info := btcjson.GetTransactionDetailsResult{ - Account: debitAccount, - Address: targetAddr, + credits := record.Credits() + debits, err := record.Debits() + var targetAddr *string + var creditAmount btcutil.Amount + if err != nil { + // Credits must be set later, but since we know the full length + // of the details slice, allocate it with the correct cap. + ret.Details = make([]btcjson.GetTransactionDetailsResult, 0, len(credits)) + } else { + ret.Details = make([]btcjson.GetTransactionDetailsResult, 1, len(credits)+1) + + details := btcjson.GetTransactionDetailsResult{ + Account: "", Category: "send", // negative since it is a send Amount: (-debits.OutputAmount(true)).ToUnit(btcutil.AmountBTC), Fee: debits.Fee().ToUnit(btcutil.AmountBTC), } - ret.Fee += info.Fee - // Add sent information to front. - ret.Details = append(ret.Details, info) + targetAddr = &details.Address + ret.Details[0] = details + ret.Fee = details.Fee + creditAmount = -debits.InputAmount() } - ret.Details = append(ret.Details, details...) - ret.Amount = totalAmount.ToUnit(btcutil.AmountBTC) - - // Generic information should be the same, so just use the first one. - first := accumulatedTxen[0] - ret.TxID = first.Tx.Tx().Sha().String() - - buf := bytes.NewBuffer(nil) - buf.Grow(first.Tx.Tx().MsgTx().SerializeSize()) - err = first.Tx.Tx().MsgTx().Serialize(buf) - if err != nil { - return nil, err - } - ret.Hex = hex.EncodeToString(buf.Bytes()) - - // TODO(oga) technically we have different time and - // timereceived depending on if a transaction was send or - // receive. We ideally should provide the correct numbers for - // both. Right now they will always be the same - ret.Time = first.Tx.Received().Unix() - ret.TimeReceived = first.Tx.Received().Unix() - if txr := first.Tx; txr.BlockHeight != -1 { - txBlock, err := txr.Block() - if err != nil { - return nil, err + for _, cred := range record.Credits() { + // Change is ignored. + if cred.Change() { + continue } - ret.BlockIndex = int64(first.Tx.Tx().Index()) - ret.BlockHash = txBlock.Hash.String() - ret.BlockTime = txBlock.Time.Unix() - ret.Confirmations = int64(txr.Confirmations(bs.Height)) + creditAmount += cred.Amount() + + var addr string + // Errors don't matter here, as we only consider the + // case where len(addrs) == 1. + _, addrs, _, _ := cred.Addresses(activeNet.Params) + if len(addrs) == 1 { + addr = addrs[0].EncodeAddress() + // The first non-change output address is considered the + // target for sent transactions. + if targetAddr != nil && *targetAddr == "" { + *targetAddr = addr + } + } + + ret.Details = append(ret.Details, btcjson.GetTransactionDetailsResult{ + Account: "", + Category: cred.Category(bs.Height).String(), + Amount: cred.Amount().ToUnit(btcutil.AmountBTC), + Address: addr, + }) } - // TODO(oga) if the tx is a coinbase we should set "generated" to true. - // Since we do not mine this currently is never the case. + + ret.Amount = creditAmount.ToUnit(btcutil.AmountBTC) return ret, nil } // ListAccounts handles a listaccounts request by returning a map of account // names to their balances. -func ListAccounts(icmd btcjson.Cmd) (interface{}, error) { - // Type assert icmd to access parameters. - cmd, ok := icmd.(*btcjson.ListAccountsCmd) - if !ok { - return nil, btcjson.ErrInternal - } +func ListAccounts(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) { + cmd := icmd.(*btcjson.ListAccountsCmd) - // Return the map. This will be marshaled into a JSON object. - return AcctMgr.ListAccounts(cmd.MinConf) -} - -// ListLockUnspent handles a listlockunspent request by returning an slice of -// all locked outpoints. -func ListLockUnspent(icmd btcjson.Cmd) (interface{}, error) { - // Due to our poor account support, this assumes only the default - // account is available. When the keystore and account heirarchies are - // reversed, the locked outpoints mapping will cover all accounts. - a, err := AcctMgr.Account("") + bal, err := w.CalculateBalance(cmd.MinConf) if err != nil { return nil, err } - return a.LockedOutpoints(), nil + // Return the map. This will be marshaled into a JSON object. + return map[string]float64{"": bal.ToUnit(btcutil.AmountBTC)}, nil +} + +// ListLockUnspent handles a listlockunspent request by returning an slice of +// all locked outpoints. +func ListLockUnspent(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) { + return w.LockedOutpoints(), nil } // ListReceivedByAddress handles a listreceivedbyaddress request by returning @@ -1822,76 +1977,58 @@ func ListLockUnspent(icmd btcjson.Cmd) (interface{}, error) { // default: one; // "includeempty": whether or not to include addresses that have no transactions - // default: false. -func ListReceivedByAddress(icmd btcjson.Cmd) (interface{}, error) { - cmd, ok := icmd.(*btcjson.ListReceivedByAddressCmd) - if !ok { - return nil, btcjson.ErrInternal - } +func ListReceivedByAddress(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) { + cmd := icmd.(*btcjson.ListReceivedByAddressCmd) // Intermediate data for each address. type AddrData struct { - // Associated account. - account *Account // Total amount received. amount btcutil.Amount // Number of confirmations of the last transaction. confirmations int32 } - rpcc, err := accessClient() - if err != nil { - return nil, err - } - bs, err := rpcc.BlockStamp() + bs, err := w.SyncedChainTip() if err != nil { return nil, err } // Intermediate data for all addresses. allAddrData := make(map[string]AddrData) - for _, account := range AcctMgr.AllAccounts() { - if cmd.IncludeEmpty { - // Create an AddrData entry for each active address in the account. - // Otherwise we'll just get addresses from transactions later. - for _, address := range account.SortedActivePaymentAddresses() { - // There might be duplicates, just overwrite them. - allAddrData[address] = AddrData{account: account} - } + if cmd.IncludeEmpty { + // Create an AddrData entry for each active address in the account. + // Otherwise we'll just get addresses from transactions later. + for _, address := range w.SortedActivePaymentAddresses() { + // There might be duplicates, just overwrite them. + allAddrData[address] = AddrData{} } - for _, record := range account.TxStore.Records() { - for _, credit := range record.Credits() { - confirmations := credit.Confirmations(bs.Height) - if !credit.Confirmed(cmd.MinConf, bs.Height) { - // Not enough confirmations, skip the current block. - continue - } - _, addresses, _, err := credit.Addresses(activeNet.Params) - if err != nil { - // Unusable address, skip it. - continue - } - for _, address := range addresses { - addrStr := address.EncodeAddress() - addrData, ok := allAddrData[addrStr] - if ok { - // Address already present, check account consistency. - if addrData.account != account { - return nil, fmt.Errorf( - "Address %v in both account %v and account %v", - addrStr, addrData.account.name, account.name) - } - addrData.amount += credit.Amount() - // Always overwrite confirmations with newer ones. - addrData.confirmations = confirmations - } else { - addrData = AddrData{ - account: account, - amount: credit.Amount(), - confirmations: confirmations, - } + } + for _, record := range w.TxStore.Records() { + for _, credit := range record.Credits() { + confirmations := credit.Confirmations(bs.Height) + if !credit.Confirmed(cmd.MinConf, bs.Height) { + // Not enough confirmations, skip the current block. + continue + } + _, addresses, _, err := credit.Addresses(activeNet.Params) + if err != nil { + // Unusable address, skip it. + continue + } + for _, address := range addresses { + addrStr := address.EncodeAddress() + addrData, ok := allAddrData[addrStr] + if ok { + addrData.amount += credit.Amount() + // Always overwrite confirmations with newer ones. + addrData.confirmations = confirmations + } else { + addrData = AddrData{ + amount: credit.Amount(), + confirmations: confirmations, } - allAddrData[addrStr] = addrData } + allAddrData[addrStr] = addrData } } } @@ -1902,7 +2039,7 @@ func ListReceivedByAddress(icmd btcjson.Cmd) (interface{}, error) { idx := 0 for address, addrData := range allAddrData { ret[idx] = btcjson.ListReceivedByAddressResult{ - Account: addrData.account.name, + Account: "", Address: address, Amount: addrData.amount.ToUnit(btcutil.AmountBTC), Confirmations: uint64(addrData.confirmations), @@ -1914,16 +2051,8 @@ func ListReceivedByAddress(icmd btcjson.Cmd) (interface{}, error) { // ListSinceBlock handles a listsinceblock request by returning an array of maps // with details of sent and received wallet transactions since the given block. -func ListSinceBlock(icmd btcjson.Cmd) (interface{}, error) { - cmd, ok := icmd.(*btcjson.ListSinceBlockCmd) - if !ok { - return nil, btcjson.ErrInternal - } - - rpcc, err := accessClient() - if err != nil { - return nil, err - } +func ListSinceBlock(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) { + cmd := icmd.(*btcjson.ListSinceBlockCmd) height := int32(-1) if cmd.BlockHash != "" { @@ -1931,14 +2060,14 @@ func ListSinceBlock(icmd btcjson.Cmd) (interface{}, error) { if err != nil { return nil, DeserializationError{err} } - block, err := rpcc.GetBlock(hash) + block, err := chainSvr.GetBlock(hash) if err != nil { return nil, err } height = int32(block.Height()) } - bs, err := rpcc.BlockStamp() + bs, err := w.SyncedChainTip() if err != nil { return nil, err } @@ -1946,12 +2075,12 @@ func ListSinceBlock(icmd btcjson.Cmd) (interface{}, error) { // For the result we need the block hash for the last block counted // in the blockchain due to confirmations. We send this off now so that // it can arrive asynchronously while we figure out the rest. - gbh := rpcc.GetBlockHashAsync(int64(bs.Height) + 1 - int64(cmd.TargetConfirmations)) + gbh := chainSvr.GetBlockHashAsync(int64(bs.Height) + 1 - int64(cmd.TargetConfirmations)) if err != nil { return nil, err } - txInfoList, err := AcctMgr.ListSinceBlock(height, bs.Height, + txInfoList, err := w.ListSinceBlock(height, bs.Height, cmd.TargetConfirmations) if err != nil { return nil, err @@ -1972,22 +2101,15 @@ func ListSinceBlock(icmd btcjson.Cmd) (interface{}, error) { // ListTransactions handles a listtransactions request by returning an // array of maps with details of sent and recevied wallet transactions. -func ListTransactions(icmd btcjson.Cmd) (interface{}, error) { - // Type assert icmd to access parameters. - cmd, ok := icmd.(*btcjson.ListTransactionsCmd) - if !ok { - return nil, btcjson.ErrInternal - } +func ListTransactions(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) { + cmd := icmd.(*btcjson.ListTransactionsCmd) - a, err := AcctMgr.Account(cmd.Account) + err := checkAccountName(cmd.Account) if err != nil { - if err == ErrNotFound { - return nil, btcjson.ErrWalletInvalidAccountName - } return nil, err } - return a.ListTransactions(cmd.From, cmd.Count) + return w.ListTransactions(cmd.From, cmd.Count) } // ListAddressTransactions handles a listaddresstransactions request by @@ -1995,18 +2117,11 @@ func ListTransactions(icmd btcjson.Cmd) (interface{}, error) { // transactions. The form of the reply is identical to listtransactions, // but the array elements are limited to transaction details which are // about the addresess included in the request. -func ListAddressTransactions(icmd btcjson.Cmd) (interface{}, error) { - // Type assert icmd to access parameters. - cmd, ok := icmd.(*btcws.ListAddressTransactionsCmd) - if !ok { - return nil, btcjson.ErrInternal - } +func ListAddressTransactions(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) { + cmd := icmd.(*btcws.ListAddressTransactionsCmd) - a, err := AcctMgr.Account(cmd.Account) + err := checkAccountName(cmd.Account) if err != nil { - if err == ErrNotFound { - return nil, btcjson.ErrWalletInvalidAccountName - } return nil, err } @@ -2024,37 +2139,27 @@ func ListAddressTransactions(icmd btcjson.Cmd) (interface{}, error) { pkHashMap[string(addr.ScriptAddress())] = struct{}{} } - return a.ListAddressTransactions(pkHashMap) + return w.ListAddressTransactions(pkHashMap) } // ListAllTransactions handles a listalltransactions request by returning // a map with details of sent and recevied wallet transactions. This is // similar to ListTransactions, except it takes only a single optional // argument for the account name and replies with all transactions. -func ListAllTransactions(icmd btcjson.Cmd) (interface{}, error) { - // Type assert icmd to access parameters. - cmd, ok := icmd.(*btcws.ListAllTransactionsCmd) - if !ok { - return nil, btcjson.ErrInternal - } +func ListAllTransactions(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) { + cmd := icmd.(*btcws.ListAllTransactionsCmd) - a, err := AcctMgr.Account(cmd.Account) + err := checkAccountName(cmd.Account) if err != nil { - if err == ErrNotFound { - return nil, btcjson.ErrWalletInvalidAccountName - } return nil, err } - return a.ListAllTransactions() + return w.ListAllTransactions() } // ListUnspent handles the listunspent command. -func ListUnspent(icmd btcjson.Cmd) (interface{}, error) { - cmd, ok := icmd.(*btcjson.ListUnspentCmd) - if !ok { - return nil, btcjson.ErrInternal - } +func ListUnspent(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) { + cmd := icmd.(*btcjson.ListUnspentCmd) addresses := make(map[string]bool) if len(cmd.Addresses) != 0 { @@ -2073,27 +2178,16 @@ func ListUnspent(icmd btcjson.Cmd) (interface{}, error) { } } - return AcctMgr.ListUnspent(cmd.MinConf, cmd.MaxConf, addresses) + return w.ListUnspent(cmd.MinConf, cmd.MaxConf, addresses) } // LockUnspent handles the lockunspent command. -func LockUnspent(icmd btcjson.Cmd) (interface{}, error) { - cmd, ok := icmd.(*btcjson.LockUnspentCmd) - if !ok { - return nil, btcjson.ErrInternal - } - - // Due to our poor account support, this assumes only the default - // account is available. When the keystore and account heirarchies are - // reversed, the locked outpoints mapping will cover all accounts. - a, err := AcctMgr.Account("") - if err != nil { - return nil, err - } +func LockUnspent(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) { + cmd := icmd.(*btcjson.LockUnspentCmd) switch { case cmd.Unlock && len(cmd.Transactions) == 0: - a.ResetLockedOutpoints() + w.ResetLockedOutpoints() default: for _, input := range cmd.Transactions { txSha, err := btcwire.NewShaHashFromStr(input.Txid) @@ -2102,9 +2196,9 @@ func LockUnspent(icmd btcjson.Cmd) (interface{}, error) { } op := btcwire.OutPoint{Hash: *txSha, Index: input.Vout} if cmd.Unlock { - a.UnlockOutpoint(op) + w.UnlockOutpoint(op) } else { - a.LockOutpoint(op) + w.LockOutpoint(op) } } } @@ -2113,23 +2207,12 @@ func LockUnspent(icmd btcjson.Cmd) (interface{}, error) { // sendPairs is a helper routine to reduce duplicated code when creating and // sending payment transactions. -func sendPairs(icmd btcjson.Cmd, account string, amounts map[string]btcutil.Amount, - minconf int) (interface{}, error) { - - rpcc, err := accessClient() - if err != nil { - return nil, err - } - - // Check that the account specified in the request exists. - a, err := AcctMgr.Account(account) - if err != nil { - return nil, btcjson.ErrWalletInvalidAccountName - } +func sendPairs(w *Wallet, chainSvr *chain.Client, cmd btcjson.Cmd, + amounts map[string]btcutil.Amount, minconf int) (interface{}, error) { // Create transaction, replying with an error if the creation // was not successful. - createdTx, err := a.txToPairs(amounts, minconf) + createdTx, err := w.txToPairs(amounts, minconf) if err != nil { switch err { case ErrNonPositiveAmount: @@ -2141,26 +2224,32 @@ func sendPairs(icmd btcjson.Cmd, account string, amounts map[string]btcutil.Amou } } - // If a change address was added, sync wallet to disk and request - // transaction notifications to the change address. - if createdTx.changeAddr != nil { - AcctMgr.ds.ScheduleWalletWrite(a) - if err := AcctMgr.ds.FlushAccount(a); err != nil { - return nil, fmt.Errorf("Cannot write account: %v", err) - } - err := rpcc.NotifyReceived([]btcutil.Address{createdTx.changeAddr}) + // Add to transaction store. + txr, err := w.TxStore.InsertTx(createdTx.tx, nil) + if err != nil { + log.Errorf("Error adding sent tx history: %v", err) + return nil, btcjson.ErrInternal + } + _, err = txr.AddDebits() + if err != nil { + log.Errorf("Error adding sent tx history: %v", err) + return nil, btcjson.ErrInternal + } + if createdTx.changeIndex >= 0 { + _, err = txr.AddCredit(uint32(createdTx.changeIndex), true) if err != nil { - return nil, err + log.Errorf("Error adding change address for sent "+ + "tx: %v", err) + return nil, btcjson.ErrInternal } } + w.TxStore.MarkDirty() - txSha, err := rpcc.SendRawTransaction(createdTx.tx.MsgTx(), false) + txSha, err := chainSvr.SendRawTransaction(createdTx.tx.MsgTx(), false) if err != nil { return nil, err } - if err := handleSendRawTxReply(icmd, txSha, a, createdTx); err != nil { - return nil, err - } + log.Infof("Successfully sent transaction %v", txSha) return txSha.String(), nil } @@ -2169,11 +2258,12 @@ func sendPairs(icmd btcjson.Cmd, account string, amounts map[string]btcutil.Amou // address. Leftover inputs not sent to the payment address or a fee for // the miner are sent back to a new address in the wallet. Upon success, // the TxID for the created transaction is returned. -func SendFrom(icmd btcjson.Cmd) (interface{}, error) { - // Type assert icmd to access parameters. - cmd, ok := icmd.(*btcjson.SendFromCmd) - if !ok { - return nil, btcjson.ErrInternal +func SendFrom(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) { + cmd := icmd.(*btcjson.SendFromCmd) + + err := checkAccountName(cmd.FromAccount) + if err != nil { + return nil, err } // Check that signed integer parameters are positive. @@ -2188,7 +2278,7 @@ func SendFrom(icmd btcjson.Cmd) (interface{}, error) { cmd.ToAddress: btcutil.Amount(cmd.Amount), } - return sendPairs(cmd, cmd.FromAccount, pairs, cmd.MinConf) + return sendPairs(w, chainSvr, cmd, pairs, cmd.MinConf) } // SendMany handles a sendmany RPC request by creating a new transaction @@ -2196,11 +2286,12 @@ func SendFrom(icmd btcjson.Cmd) (interface{}, error) { // payment addresses. Leftover inputs not sent to the payment address // or a fee for the miner are sent back to a new address in the wallet. // Upon success, the TxID for the created transaction is returned. -func SendMany(icmd btcjson.Cmd) (interface{}, error) { - // Type assert icmd to access parameters. - cmd, ok := icmd.(*btcjson.SendManyCmd) - if !ok { - return nil, btcjson.ErrInternal +func SendMany(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) { + cmd := icmd.(*btcjson.SendManyCmd) + + err := checkAccountName(cmd.FromAccount) + if err != nil { + return nil, err } // Check that minconf is positive. @@ -2214,7 +2305,7 @@ func SendMany(icmd btcjson.Cmd) (interface{}, error) { pairs[k] = btcutil.Amount(v) } - return sendPairs(cmd, cmd.FromAccount, pairs, cmd.MinConf) + return sendPairs(w, chainSvr, cmd, pairs, cmd.MinConf) } // SendToAddress handles a sendtoaddress RPC request by creating a new @@ -2222,12 +2313,8 @@ func SendMany(icmd btcjson.Cmd) (interface{}, error) { // payment address. Leftover inputs not sent to the payment address or a fee // for the miner are sent back to a new address in the wallet. Upon success, // the TxID for the created transaction is returned. -func SendToAddress(icmd btcjson.Cmd) (interface{}, error) { - // Type assert icmd to access parameters. - cmd, ok := icmd.(*btcjson.SendToAddressCmd) - if !ok { - return nil, btcjson.ErrInternal - } +func SendToAddress(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) { + cmd := icmd.(*btcjson.SendToAddressCmd) // Check that signed integer parameters are positive. if cmd.Amount < 0 { @@ -2239,103 +2326,19 @@ func SendToAddress(icmd btcjson.Cmd) (interface{}, error) { cmd.Address: btcutil.Amount(cmd.Amount), } - return sendPairs(cmd, "", pairs, 1) -} - -func handleSendRawTxReply(icmd btcjson.Cmd, txSha *btcwire.ShaHash, a *Account, txInfo *CreatedTx) error { - // Add to transaction store. - txr, err := a.TxStore.InsertTx(txInfo.tx, nil) - if err != nil { - log.Errorf("Error adding sent tx history: %v", err) - return btcjson.ErrInternal - } - debits, err := txr.AddDebits(txInfo.inputs) - if err != nil { - log.Errorf("Error adding sent tx history: %v", err) - return btcjson.ErrInternal - } - AcctMgr.ds.ScheduleTxStoreWrite(a) - - // Notify websocket clients of the transaction. - rpcc, err := accessClient() - if err != nil { - return err - } - if bs, err := rpcc.BlockStamp(); err == nil { - ltr, err := debits.ToJSON(a.KeyStore.Name(), bs.Height, - a.KeyStore.Net()) - if err != nil { - log.Errorf("Error adding sent tx history: %v", err) - return btcjson.ErrInternal - } - for _, details := range ltr { - server.NotifyNewTxDetails(a.KeyStore.Name(), details) - } - } - - // Disk sync tx and utxo stores. - if err := AcctMgr.ds.FlushAccount(a); err != nil { - log.Errorf("Cannot write account: %v", err) - return err - } - - // Notify websocket clients of account's new unconfirmed and - // confirmed balance. - confirmed, err := a.CalculateBalance(1) - var unconfirmed btcutil.Amount - if err == nil { - unconfirmed, err = a.CalculateBalance(0) - } - if err != nil { - return err - } - unconfirmed -= confirmed - server.NotifyWalletBalance(a.name, confirmed) - server.NotifyWalletBalanceUnconfirmed(a.name, unconfirmed) - - // The comments to be saved differ based on the underlying type - // of the cmd, so switch on the type to check whether it is a - // SendFromCmd or SendManyCmd. - // - // TODO(jrick): If message succeeded in being sent, save the - // transaction details with comments. - switch cmd := icmd.(type) { - case *btcjson.SendFromCmd: - _ = cmd.Comment - _ = cmd.CommentTo - - case *btcjson.SendManyCmd: - _ = cmd.Comment - case *btcjson.SendToAddressCmd: - _ = cmd.Comment - _ = cmd.CommentTo - } - - log.Infof("Successfully sent transaction %v", txSha) - return nil + return sendPairs(w, chainSvr, cmd, pairs, 1) } // SetTxFee sets the transaction fee per kilobyte added to transactions. -func SetTxFee(icmd btcjson.Cmd) (interface{}, error) { - // Type assert icmd to access parameters. - cmd, ok := icmd.(*btcjson.SetTxFeeCmd) - if !ok { - return nil, btcjson.ErrInternal - } +func SetTxFee(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) { + cmd := icmd.(*btcjson.SetTxFeeCmd) // Check that amount is not negative. if cmd.Amount < 0 { return nil, ErrNeedPositiveAmount } - // Lookup default account (which realistically is the only account - // that transactions can be made with at the moment) and set its - // fee increment field. - a, err := AcctMgr.Account("") - if err != nil { - return nil, ErrNoAccounts - } - a.FeeIncrement = btcutil.Amount(cmd.Amount) + w.FeeIncrement = btcutil.Amount(cmd.Amount) // A boolean true result is returned upon success. return true, nil @@ -2343,19 +2346,15 @@ func SetTxFee(icmd btcjson.Cmd) (interface{}, error) { // SignMessage signs the given message with the private key for the given // address -func SignMessage(icmd btcjson.Cmd) (interface{}, error) { - // Type assert icmd to access parameters. - cmd, ok := icmd.(*btcjson.SignMessageCmd) - if !ok { - return nil, btcjson.ErrInternal - } +func SignMessage(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) { + cmd := icmd.(*btcjson.SignMessageCmd) addr, err := btcutil.DecodeAddress(cmd.Address, activeNet.Params) if err != nil { return nil, ParseError{err} } - ainfo, err := AcctMgr.Address(addr) + ainfo, err := w.KeyStore.Address(addr) if err != nil { return nil, btcjson.ErrInvalidAddressOrKey } @@ -2376,48 +2375,67 @@ func SignMessage(icmd btcjson.Cmd) (interface{}, error) { return base64.StdEncoding.EncodeToString(sigbytes), nil } -// CreateEncryptedWallet creates a new account with an encrypted -// wallet. If an account with the same name as the requested account -// name already exists, an invalid account name error is returned to -// the client. -// -// Wallets will be created on TestNet3, or MainNet if btcwallet is run with -// the --mainnet option. -func CreateEncryptedWallet(icmd btcjson.Cmd) (interface{}, error) { - // Type assert icmd to access parameters. - cmd, ok := icmd.(*btcws.CreateEncryptedWalletCmd) - if !ok { - return nil, btcjson.ErrInternal +func (s *rpcServer) handleCreateEncryptedWallet(request []byte) (interface{}, error) { + s.handlerLock.Lock() + defer s.handlerLock.Unlock() + + switch { + case s.wallet == nil && !s.createOK: + // Wallet hasn't finished loading, SetWallet (either with an + // actual or nil wallet) hasn't been called yet. + return nil, ErrUnloadedWallet + + case s.wallet != nil: + return nil, errors.New("wallet already opened") + + case s.chainSvr == nil: + return nil, ErrNeedsChainSvr } - err := AcctMgr.CreateEncryptedWallet([]byte(cmd.Passphrase)) + // Parse request to access the passphrase. + cmd, err := btcjson.ParseMarshaledCmd(request) if err != nil { - if err == ErrWalletExists { - return nil, btcjson.ErrWalletInvalidAccountName - } return nil, err } + req, ok := cmd.(*btcws.CreateEncryptedWalletCmd) + if !ok || len(req.Passphrase) == 0 { + // Request is already valid JSON-RPC and the method was good, + // so must be bad parameters. + return nil, btcjson.ErrInvalidParams + } + + wallet, err := newEncryptedWallet([]byte(req.Passphrase), s.chainSvr) + if err != nil { + return nil, err + } + + s.wallet = wallet + s.handlerLock = noopLocker{} + s.handlerLookup = lookupAnyHandler + + wallet.Start(s.chainSvr) + + // When the wallet eventually shuts down (i.e. from the stop RPC), close + // the rest of the server. + go func() { + wallet.WaitForShutdown() + s.Stop() + }() // A nil reply is sent upon successful wallet creation. return nil, nil } // RecoverAddresses recovers the next n addresses from an account's wallet. -func RecoverAddresses(icmd btcjson.Cmd) (interface{}, error) { - cmd, ok := icmd.(*btcws.RecoverAddressesCmd) - if !ok { - return nil, btcjson.ErrInternal - } +func RecoverAddresses(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) { + cmd := icmd.(*btcws.RecoverAddressesCmd) - a, err := AcctMgr.Account(cmd.Account) + err := checkDefaultAccount(cmd.Account) if err != nil { - if err == ErrNotFound { - return nil, btcjson.ErrWalletInvalidAccountName - } return nil, err } - err = a.RecoverAddresses(cmd.N) + err = w.RecoverAddresses(cmd.N) return nil, err } @@ -2429,11 +2447,8 @@ type pendingTx struct { } // SignRawTransaction handles the signrawtransaction command. -func SignRawTransaction(icmd btcjson.Cmd) (interface{}, error) { - cmd, ok := icmd.(*btcjson.SignRawTransactionCmd) - if !ok { - return nil, btcjson.ErrInternal - } +func SignRawTransaction(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) { + cmd := icmd.(*btcjson.SignRawTransactionCmd) serializedTx, err := decodeHexStr(cmd.RawTx) if err != nil { @@ -2487,8 +2502,6 @@ func SignRawTransaction(icmd btcjson.Cmd) (interface{}, error) { }] = script } - var rpcc *rpcClient - // Now we go and look for any inputs that we were not provided by // querying btcd with getrawtransaction. We queue up a bunch of async // requests and will wait for replies after we have checked the rest of @@ -2512,15 +2525,9 @@ func SignRawTransaction(icmd btcjson.Cmd) (interface{}, error) { } // Never heard of this one before, request it. - if rpcc == nil { - rpcc, err = accessClient() - if err != nil { - return nil, err - } - } prevHash := &txIn.PreviousOutpoint.Hash requested[txIn.PreviousOutpoint.Hash] = &pendingTx{ - resp: rpcc.GetRawTransactionAsync(prevHash), + resp: chainSvr.GetRawTransactionAsync(prevHash), inputs: []uint32{txIn.PreviousOutpoint.Index}, } } @@ -2627,7 +2634,7 @@ func SignRawTransaction(icmd btcjson.Cmd) (interface{}, error) { } return wif.PrivKey.ToECDSA(), wif.CompressPubKey, nil } - address, err := AcctMgr.Address(addr) + address, err := w.KeyStore.Address(addr) if err != nil { return nil, false, err } @@ -2658,7 +2665,7 @@ func SignRawTransaction(icmd btcjson.Cmd) (interface{}, error) { } return script, nil } - address, err := AcctMgr.Address(addr) + address, err := w.KeyStore.Address(addr) if err != nil { return nil, err } @@ -2702,12 +2709,12 @@ func SignRawTransaction(icmd btcjson.Cmd) (interface{}, error) { } } - buf := bytes.NewBuffer(nil) + var buf bytes.Buffer buf.Grow(msgTx.SerializeSize()) // All returned errors (not OOM, which panics) encounted during // bytes.Buffer writes are unexpected. - if err = msgTx.Serialize(buf); err != nil { + if err = msgTx.Serialize(&buf); err != nil { panic(err) } @@ -2717,24 +2724,14 @@ func SignRawTransaction(icmd btcjson.Cmd) (interface{}, error) { }, nil } -// Stop handles the stop command by shutting down the process after the request -// is handled. -func Stop(icmd btcjson.Cmd) (interface{}, error) { - server.Stop() - return "btcwallet stopping.", nil -} - // ValidateAddress handles the validateaddress command. -func ValidateAddress(icmd btcjson.Cmd) (interface{}, error) { - cmd, ok := icmd.(*btcjson.ValidateAddressCmd) - if !ok { - return nil, btcjson.ErrInternal - } +func ValidateAddress(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) { + cmd := icmd.(*btcjson.ValidateAddressCmd) result := btcjson.ValidateAddressResult{} addr, err := btcutil.DecodeAddress(cmd.Address, activeNet.Params) if err != nil { - // Use zero value (false) for IsValid. + // Use result zero value (IsValid=false). return result, nil } @@ -2745,18 +2742,10 @@ func ValidateAddress(icmd btcjson.Cmd) (interface{}, error) { result.Address = addr.EncodeAddress() result.IsValid = true - // We can't use AcctMgr.Address() here since we also need the account - // name. - if account, err := AcctMgr.AccountByAddress(addr); err == nil { - // The address must be handled by this account, so we expect - // this call to succeed without error. - ainfo, err := account.KeyStore.Address(addr) - if err != nil { - panic(err) - } - + ainfo, err := w.KeyStore.Address(addr) + if err == nil { result.IsMine = true - result.Account = account.name + result.Account = "" if pka, ok := ainfo.(keystore.PubKeyAddress); ok { result.IsCompressed = pka.Compressed() @@ -2786,19 +2775,23 @@ func ValidateAddress(icmd btcjson.Cmd) (interface{}, error) { // VerifyMessage handles the verifymessage command by verifying the provided // compact signature for the given address and message. -func VerifyMessage(icmd btcjson.Cmd) (interface{}, error) { - cmd, ok := icmd.(*btcjson.VerifyMessageCmd) - if !ok { - return nil, btcjson.ErrInternal - } +func VerifyMessage(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) { + cmd := icmd.(*btcjson.VerifyMessageCmd) addr, err := btcutil.DecodeAddress(cmd.Address, activeNet.Params) if err != nil { return nil, ParseError{err} } + switch addr.(type) { + case *btcutil.AddressPubKeyHash: // ok + case *btcutil.AddressPubKey: // ok + default: + return nil, errors.New("address type not supported") + } + // First check we know about the address and get the keys. - ainfo, err := AcctMgr.Address(addr) + ainfo, err := w.KeyStore.Address(addr) if err != nil { return nil, btcjson.ErrInvalidAddressOrKey } @@ -2832,30 +2825,26 @@ func VerifyMessage(icmd btcjson.Cmd) (interface{}, error) { // WalletIsLocked handles the walletislocked extension request by // returning the current lock state (false for unlocked, true for locked) // of an account. -func WalletIsLocked(icmd btcjson.Cmd) (interface{}, error) { - return !<-AcctMgr.unlockedState, nil +func WalletIsLocked(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) { + return w.Locked(), nil } // WalletLock handles a walletlock request by locking the all account // wallets, returning an error if any wallet is not encrypted (for example, // a watching-only wallet). -func WalletLock(icmd btcjson.Cmd) (interface{}, error) { - AcctMgr.LockWallets() +func WalletLock(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) { + w.Lock() return nil, nil } // WalletPassphrase responds to the walletpassphrase request by unlocking // the wallet. The decryption key is saved in the wallet until timeout // seconds expires, after which the wallet is locked. -func WalletPassphrase(icmd btcjson.Cmd) (interface{}, error) { - // Type assert icmd to access parameters. - cmd, ok := icmd.(*btcjson.WalletPassphraseCmd) - if !ok { - return nil, btcjson.ErrInternal - } +func WalletPassphrase(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) { + cmd := icmd.(*btcjson.WalletPassphraseCmd) timeout := time.Second * time.Duration(cmd.Timeout) - err := AcctMgr.UnlockWallets([]byte(cmd.Passphrase), timeout) + err := w.Unlock([]byte(cmd.Passphrase), timeout) return nil, err } @@ -2866,13 +2855,10 @@ func WalletPassphrase(icmd btcjson.Cmd) (interface{}, error) { // // If the old passphrase is correct and the passphrase is changed, all // wallets will be immediately locked. -func WalletPassphraseChange(icmd btcjson.Cmd) (interface{}, error) { - cmd, ok := icmd.(*btcjson.WalletPassphraseChangeCmd) - if !ok { - return nil, btcjson.ErrInternal - } +func WalletPassphraseChange(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) { + cmd := icmd.(*btcjson.WalletPassphraseChangeCmd) - err := AcctMgr.ChangePassphrase([]byte(cmd.OldPassphrase), + err := w.ChangePassphrase([]byte(cmd.OldPassphrase), []byte(cmd.NewPassphrase)) if err == keystore.ErrWrongPassphrase { return nil, btcjson.ErrWalletPassphraseIncorrect @@ -2880,81 +2866,6 @@ func WalletPassphraseChange(icmd btcjson.Cmd) (interface{}, error) { return nil, err } -// AccountNtfn is a struct for marshalling any generic notification -// about a account for a websocket client. -// -// TODO(jrick): move to btcjson so it can be shared with clients? -type AccountNtfn struct { - Account string `json:"account"` - Notification interface{} `json:"notification"` -} - -// NotifyWalletLockStateChange sends a notification to all websocket clients -// that the wallet has just been locked or unlocked. -func (s *rpcServer) NotifyWalletLockStateChange(account string, locked bool) { - ntfn := btcws.NewWalletLockStateNtfn(account, locked) - mntfn, err := ntfn.MarshalJSON() - // If the marshal failed, it indicates that the btcws notification - // struct contains a field with a type that is not marshalable. - if err != nil { - panic(err) - } - select { - case s.broadcasts <- mntfn: - case <-s.quit: - } -} - -// NotifyWalletBalance sends a confirmed account balance notification -// to all websocket clients. -func (s *rpcServer) NotifyWalletBalance(account string, balance btcutil.Amount) { - fbal := balance.ToUnit(btcutil.AmountBTC) - ntfn := btcws.NewAccountBalanceNtfn(account, fbal, true) - mntfn, err := ntfn.MarshalJSON() - // If the marshal failed, it indicates that the btcws notification - // struct contains a field with a type that is not marshalable. - if err != nil { - panic(err) - } - select { - case s.broadcasts <- mntfn: - case <-s.quit: - } -} - -// NotifyWalletBalanceUnconfirmed sends a confirmed account balance -// notification to all websocket clients. -func (s *rpcServer) NotifyWalletBalanceUnconfirmed(account string, balance btcutil.Amount) { - fbal := balance.ToUnit(btcutil.AmountBTC) - ntfn := btcws.NewAccountBalanceNtfn(account, fbal, false) - mntfn, err := ntfn.MarshalJSON() - // If the marshal failed, it indicates that the btcws notification - // struct contains a field with a type that is not marshalable. - if err != nil { - panic(err) - } - select { - case s.broadcasts <- mntfn: - case <-s.quit: - } -} - -// NotifyNewTxDetails sends details of a new transaction to all websocket -// clients. -func (s *rpcServer) NotifyNewTxDetails(account string, details btcjson.ListTransactionsResult) { - ntfn := btcws.NewTxNtfn(account, &details) - mntfn, err := ntfn.MarshalJSON() - // If the marshal failed, it indicates that the btcws notification - // struct contains a field with a type that is not marshalable. - if err != nil { - panic(err) - } - select { - case s.broadcasts <- mntfn: - case <-s.quit: - } -} - // decodeHexStr decodes the hex encoding of a string, possibly prepending a // leading '0' character if there is an odd number of bytes in the hex string. // This is to prevent an error for an invalid hex string when using an odd diff --git a/txstore/json.go b/txstore/json.go index 905fde5..e8c94ef 100644 --- a/txstore/json.go +++ b/txstore/json.go @@ -29,16 +29,19 @@ import ( func (t *TxRecord) ToJSON(account string, chainHeight int32, net *btcnet.Params) ([]btcjson.ListTransactionsResult, error) { + t.s.mtx.RLock() + defer t.s.mtx.RUnlock() + results := []btcjson.ListTransactionsResult{} if d, err := t.Debits(); err == nil { - r, err := d.ToJSON(account, chainHeight, net) + r, err := d.toJSON(account, chainHeight, net) if err != nil { return nil, err } results = r } for _, c := range t.Credits() { - r, err := c.ToJSON(account, chainHeight, net) + r, err := c.toJSON(account, chainHeight, net) if err != nil { return nil, err } @@ -49,7 +52,16 @@ func (t *TxRecord) ToJSON(account string, chainHeight int32, // ToJSON returns a slice of objects that may be marshaled as a JSON array // of JSON objects for a listtransactions RPC reply. -func (d *Debits) ToJSON(account string, chainHeight int32, +func (d Debits) ToJSON(account string, chainHeight int32, + net *btcnet.Params) ([]btcjson.ListTransactionsResult, error) { + + d.s.mtx.RLock() + defer d.s.mtx.RUnlock() + + return d.toJSON(account, chainHeight, net) +} + +func (d Debits) toJSON(account string, chainHeight int32, net *btcnet.Params) ([]btcjson.ListTransactionsResult, error) { msgTx := d.Tx().MsgTx() @@ -82,7 +94,7 @@ func (d *Debits) ToJSON(account string, chainHeight int32, result.BlockHash = b.Hash.String() result.BlockIndex = int64(d.Tx().Index()) result.BlockTime = b.Time.Unix() - result.Confirmations = int64(d.Confirmations(chainHeight)) + result.Confirmations = int64(confirms(d.BlockHeight, chainHeight)) } reply = append(reply, result) } @@ -102,11 +114,18 @@ const ( CreditImmature ) -// Category returns the category of the credit. The passed block chain height is +// category returns the category of the credit. The passed block chain height is // used to distinguish immature from mature coinbase outputs. func (c *Credit) Category(chainHeight int32) CreditCategory { - if c.IsCoinbase() { - if c.Confirmed(btcchain.CoinbaseMaturity, chainHeight) { + c.s.mtx.RLock() + defer c.s.mtx.RUnlock() + + return c.category(chainHeight) +} + +func (c *Credit) category(chainHeight int32) CreditCategory { + if c.isCoinbase() { + if confirmed(btcchain.CoinbaseMaturity, c.BlockHeight, chainHeight) { return CreditGenerate } return CreditImmature @@ -132,7 +151,16 @@ func (c CreditCategory) String() string { // ToJSON returns a slice of objects that may be marshaled as a JSON array // of JSON objects for a listtransactions RPC reply. -func (c *Credit) ToJSON(account string, chainHeight int32, +func (c Credit) ToJSON(account string, chainHeight int32, + net *btcnet.Params) (btcjson.ListTransactionsResult, error) { + + c.s.mtx.RLock() + defer c.s.mtx.RUnlock() + + return c.toJSON(account, chainHeight, net) +} + +func (c Credit) toJSON(account string, chainHeight int32, net *btcnet.Params) (btcjson.ListTransactionsResult, error) { msgTx := c.Tx().MsgTx() @@ -146,7 +174,7 @@ func (c *Credit) ToJSON(account string, chainHeight int32, result := btcjson.ListTransactionsResult{ Account: account, - Category: c.Category(chainHeight).String(), + Category: c.category(chainHeight).String(), Address: address, Amount: btcutil.Amount(txout.Value).ToUnit(btcutil.AmountBTC), TxID: c.Tx().Sha().String(), @@ -163,7 +191,7 @@ func (c *Credit) ToJSON(account string, chainHeight int32, result.BlockHash = b.Hash.String() result.BlockIndex = int64(c.Tx().Index()) result.BlockTime = b.Time.Unix() - result.Confirmations = int64(c.Confirmations(chainHeight)) + result.Confirmations = int64(confirms(c.BlockHeight, chainHeight)) } return result, nil diff --git a/txstore/notifications.go b/txstore/notifications.go new file mode 100644 index 0000000..6763868 --- /dev/null +++ b/txstore/notifications.go @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2014 Conformal Systems LLC + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +package txstore + +import ( + "errors" +) + +// ErrDuplicateListen is returned for any attempts to listen for the same +// notification more than once. If callers must pass along a notifiation to +// multiple places, they must broadcast it themself. +var ErrDuplicateListen = errors.New("duplicate listen") + +type noopLocker struct{} + +func (noopLocker) Lock() {} +func (noopLocker) Unlock() {} + +func (s *Store) updateNotificationLock() { + switch { + case s.newCredit == nil: + fallthrough + case s.newDebits == nil: + fallthrough + case s.minedCredit == nil: + fallthrough + case s.minedDebits == nil: + return + } + s.notificationLock = noopLocker{} +} + +// ListenNewCredits returns a channel that passes all Credits that are newly +// added to the transaction store. The channel must be read, or other +// transaction store methods will block. +// +// If this is called twice, ErrDuplicateListen is returned. +func (s *Store) ListenNewCredits() (<-chan Credit, error) { + s.notificationLock.Lock() + defer s.notificationLock.Unlock() + + if s.newCredit != nil { + return nil, ErrDuplicateListen + } + s.newCredit = make(chan Credit) + s.updateNotificationLock() + return s.newCredit, nil +} + +// ListenNewDebits returns a channel that passes all Debits that are newly +// added to the transaction store. The channel must be read, or other +// transaction store methods will block. +// +// If this is called twice, ErrDuplicateListen is returned. +func (s *Store) ListenNewDebits() (<-chan Debits, error) { + s.notificationLock.Lock() + defer s.notificationLock.Unlock() + + if s.newDebits != nil { + return nil, ErrDuplicateListen + } + s.newDebits = make(chan Debits) + s.updateNotificationLock() + return s.newDebits, nil +} + +// ListenMinedCredits returns a channel that passes all that are moved +// from unconfirmed to a newly attached block. The channel must be read, or +// other transaction store methods will block. +// +// If this is called twice, ErrDuplicateListen is returned. +func (s *Store) ListenMinedCredits() (<-chan Credit, error) { + s.notificationLock.Lock() + defer s.notificationLock.Unlock() + + if s.minedCredit != nil { + return nil, ErrDuplicateListen + } + s.minedCredit = make(chan Credit) + s.updateNotificationLock() + return s.minedCredit, nil +} + +// ListenMinedDebits returns a channel that passes all Debits that are moved +// from unconfirmed to a newly attached block. The channel must be read, or +// other transaction store methods will block. +// +// If this is called twice, ErrDuplicateListen is returned. +func (s *Store) ListenMinedDebits() (<-chan Debits, error) { + s.notificationLock.Lock() + defer s.notificationLock.Unlock() + + if s.minedDebits != nil { + return nil, ErrDuplicateListen + } + s.minedDebits = make(chan Debits) + s.updateNotificationLock() + return s.minedDebits, nil +} + +func (s *Store) notifyNewCredit(c Credit) { + s.notificationLock.Lock() + if s.newCredit != nil { + s.newCredit <- c + } + s.notificationLock.Unlock() +} + +func (s *Store) notifyNewDebits(d Debits) { + s.notificationLock.Lock() + if s.newDebits != nil { + s.newDebits <- d + } + s.notificationLock.Unlock() +} + +func (s *Store) notifyMinedCredit(c Credit) { + s.notificationLock.Lock() + if s.minedCredit != nil { + s.minedCredit <- c + } + s.notificationLock.Unlock() +} + +func (s *Store) notifyMinedDebits(d Debits) { + s.notificationLock.Lock() + if s.minedDebits != nil { + s.minedDebits <- d + } + s.notificationLock.Unlock() +} diff --git a/txstore/serialization.go b/txstore/serialization.go index 3f8197f..401ffb6 100644 --- a/txstore/serialization.go +++ b/txstore/serialization.go @@ -22,12 +22,20 @@ import ( "errors" "fmt" "io" + "io/ioutil" + "os" + "path/filepath" "time" "github.com/conformal/btcutil" + "github.com/conformal/btcwallet/rename" "github.com/conformal/btcwire" ) +// filename is the name of the file typically used to save a transaction +// store on disk. +const filename = "tx.bin" + // All Store versions (both old and current). const ( versFirst uint32 = iota @@ -59,6 +67,8 @@ var byteOrder = binary.LittleEndian // ReadFrom satisifies the io.ReaderFrom interface by deserializing a // transaction store from an io.Reader. func (s *Store) ReadFrom(r io.Reader) (int64, error) { + // Don't bother locking this. The mutex gets overwritten anyways. + var buf [4]byte uint32Bytes := buf[:4] @@ -76,7 +86,7 @@ func (s *Store) ReadFrom(r io.Reader) (int64, error) { } // Reset store. - *s = *New() + *s = *New(s.path) // Read block structures. Begin by reading the total number of block // structures to be read, and then iterate that many times to read @@ -144,6 +154,13 @@ func (s *Store) ReadFrom(r io.Reader) (int64, error) { // WriteTo satisifies the io.WriterTo interface by serializing a transaction // store to an io.Writer. func (s *Store) WriteTo(w io.Writer) (int64, error) { + s.mtx.RLock() + defer s.mtx.RUnlock() + + return s.writeTo(w) +} + +func (s *Store) writeTo(w io.Writer) (int64, error) { var buf [4]byte uint32Bytes := buf[:4] @@ -1128,3 +1145,72 @@ func (u *unconfirmedStore) WriteTo(w io.Writer) (int64, error) { return n64, nil } + +// TODO: set this automatically. +func (s *Store) MarkDirty() { + s.mtx.Lock() + defer s.mtx.Unlock() + + s.dirty = true +} + +func (s *Store) WriteIfDirty() error { + s.mtx.RLock() + if !s.dirty { + s.mtx.RUnlock() + return nil + } + + // TempFile creates the file 0600, so no need to chmod it. + fi, err := ioutil.TempFile(s.dir, s.file) + if err != nil { + s.mtx.RUnlock() + return err + } + fiPath := fi.Name() + + _, err = s.writeTo(fi) + if err != nil { + s.mtx.RUnlock() + fi.Close() + return err + } + err = fi.Sync() + if err != nil { + s.mtx.RUnlock() + fi.Close() + return err + } + fi.Close() + + err = rename.Atomic(fiPath, s.path) + s.mtx.RUnlock() + if err == nil { + s.mtx.Lock() + s.dirty = false + s.mtx.Unlock() + } + + return err +} + +// OpenDir opens a new transaction store from the specified directory. +// If the file does not exist, the error from the os package will be +// returned, and can be checked with os.IsNotExist to differentiate missing +// file errors from others (including deserialization). +func OpenDir(dir string) (*Store, error) { + path := filepath.Join(dir, filename) + fi, err := os.OpenFile(path, os.O_RDONLY, 0) + if err != nil { + return nil, err + } + defer fi.Close() + store := new(Store) + _, err = store.ReadFrom(fi) + if err != nil { + return nil, err + } + store.path = path + store.dir = dir + return store, nil +} diff --git a/txstore/tx.go b/txstore/tx.go index 6181d06..6b49e38 100644 --- a/txstore/tx.go +++ b/txstore/tx.go @@ -19,7 +19,9 @@ package txstore import ( "errors" "fmt" + "path/filepath" "sort" + "sync" "time" "github.com/conformal/btcchain" @@ -155,6 +157,15 @@ type blockAmounts struct { // Store implements a transaction store for storing and managing wallet // transactions. type Store struct { + // TODO: Use atomic operations for dirty so the reader lock + // doesn't need to be grabbed. + dirty bool + path string + dir string + file string + + mtx sync.RWMutex + // blocks holds wallet transaction records for each block they appear // in. This is sorted by block height in increasing order. A separate // map is included to lookup indexes for blocks at some height. @@ -166,6 +177,15 @@ type Store struct { // unconfirmed holds a collection of wallet transactions that have not // been mined into a block yet. unconfirmed unconfirmedStore + + // Channels to notify callers of changes to the transaction store. + // These are only created when a caller calls the appropiate + // registration method. + newCredit chan Credit + newDebits chan Debits + minedCredit chan Credit + minedDebits chan Debits + notificationLock sync.Locker } // blockTxCollection holds a collection of wallet transactions from exactly one @@ -254,8 +274,11 @@ type credit struct { } // New allocates and initializes a new transaction store. -func New() *Store { +func New(dir string) *Store { return &Store{ + path: filepath.Join(dir, filename), + dir: dir, + file: filename, blockIndexes: map[int32]uint32{}, unspent: map[btcwire.OutPoint]BlockTxKey{}, unconfirmed: unconfirmedStore{ @@ -265,6 +288,7 @@ func New() *Store { spentUnconfirmed: map[btcwire.OutPoint]*txRecord{}, previousOutpoints: map[btcwire.OutPoint]*txRecord{}, }, + notificationLock: new(sync.Mutex), } } @@ -443,6 +467,10 @@ func (s *Store) moveMinedTx(r *txRecord, block *Block) error { // debits should already be non-nil r.debits.spends = append(r.debits.spends, prev) } + if r.debits != nil { + d := Debits{&TxRecord{key, r, s}} + s.notifyMinedDebits(d) + } // For each credit in r, if the credit is spent by another unconfirmed // transaction, move the spending transaction from spentUnconfirmed @@ -475,6 +503,9 @@ func (s *Store) moveMinedTx(r *txRecord, block *Block) error { value := r.Tx().MsgTx().TxOut[i].Value b.amountDeltas.Spendable += btcutil.Amount(value) } + + c := Credit{&TxRecord{key, r, s}, op.Index} + s.notifyMinedCredit(c) } // If this moved transaction debits from any previous credits, decrement @@ -496,6 +527,9 @@ func (s *Store) moveMinedTx(r *txRecord, block *Block) error { // The transaction record is returned. Credits and debits may be added to the // transaction by calling methods on the TxRecord. func (s *Store) InsertTx(tx *btcutil.Tx, block *Block) (*TxRecord, error) { + s.mtx.Lock() + defer s.mtx.Unlock() + // The receive time will be the earlier of now and the block time // (if any). received := time.Now() @@ -565,12 +599,18 @@ func (s *Store) InsertTx(tx *btcutil.Tx, block *Block) (*TxRecord, error) { // Received returns the earliest known time the transaction was received by. func (t *TxRecord) Received() time.Time { + t.s.mtx.RLock() + defer t.s.mtx.RUnlock() + return t.received } // Block returns the block details for a transaction. If the transaction is // unmined, both the block and returned error are nil. func (t *TxRecord) Block() (*Block, error) { + t.s.mtx.RLock() + defer t.s.mtx.RUnlock() + coll, err := t.s.lookupBlock(t.BlockHeight) if err != nil { if err == MissingBlockError(-1) { @@ -581,56 +621,37 @@ func (t *TxRecord) Block() (*Block, error) { return &coll.Block, nil } -// AddDebits marks a transaction record as having debited from all them transaction -// credits in the spent slice. If spent is nil, the previous debits will be found, -// however this is an expensive lookup and should be avoided if possible. -func (t *TxRecord) AddDebits(spent []Credit) (Debits, error) { - if t.debits == nil { - // Find now-spent credits if no debits have been previously set - // and none were passed in by the caller. - if len(spent) == 0 { - foundSpent, err := t.s.findPreviousCredits(t.Tx()) - if err != nil { - return Debits{}, err - } - spent = foundSpent - } +// AddDebits marks a transaction record as having debited from previous wallet +// credits. +func (t *TxRecord) AddDebits() (Debits, error) { + t.s.mtx.Lock() + defer t.s.mtx.Unlock() + if t.debits == nil { + spent, err := t.s.findPreviousCredits(t.Tx()) + if err != nil { + return Debits{}, err + } debitAmount, err := t.s.markOutputsSpent(spent, t) if err != nil { return Debits{}, err } - t.debits = &debits{amount: debitAmount} + + prevOutputKeys := make([]BlockOutputKey, len(spent)) + for i, c := range spent { + prevOutputKeys[i] = c.outputKey() + } + + t.debits = &debits{amount: debitAmount, spends: prevOutputKeys} log.Debugf("Transaction %v spends %d previously-unspent "+ "%s totaling %v", t.tx.Sha(), len(spent), pickNoun(len(spent), "output", "outputs"), debitAmount) } - switch t.BlockHeight { - case -1: // unconfimred - for _, c := range spent { - op := c.OutPoint() - switch c.BlockHeight { - case -1: // unconfirmed - t.s.unconfirmed.spentUnconfirmed[*op] = t.txRecord - default: - key := c.outputKey() - t.s.unconfirmed.spentBlockOutPointKeys[*op] = key - t.s.unconfirmed.spentBlockOutPoints[key] = t.txRecord - } - } - - default: - if t.debits.spends == nil { - prevOutputKeys := make([]BlockOutputKey, len(spent)) - for i, c := range spent { - prevOutputKeys[i] = c.outputKey() - } - t.txRecord.debits.spends = prevOutputKeys - } - } - return Debits{t}, nil + d := Debits{t} + t.s.notifyNewDebits(d) + return d, nil } // findPreviousCredits searches for all unspent credits that make up the inputs @@ -699,10 +720,18 @@ func (s *Store) findPreviousCredits(tx *btcutil.Tx) ([]Credit, error) { func (s *Store) markOutputsSpent(spent []Credit, t *TxRecord) (btcutil.Amount, error) { var a btcutil.Amount for _, prev := range spent { - op := prev.OutPoint() + op := prev.outPoint() switch prev.BlockHeight { case -1: // unconfirmed - s.unconfirmed.spentUnconfirmed[*op] = t.txRecord + op := prev.outPoint() + switch prev.BlockHeight { + case -1: // unconfirmed + t.s.unconfirmed.spentUnconfirmed[*op] = t.txRecord + default: + key := prev.outputKey() + t.s.unconfirmed.spentBlockOutPointKeys[*op] = key + t.s.unconfirmed.spentBlockOutPoints[key] = t.txRecord + } default: // Update spent info. @@ -722,7 +751,7 @@ func (s *Store) markOutputsSpent(spent []Credit, t *TxRecord) (btcutil.Amount, e } // Increment total debited amount. - a += prev.Amount() + a += prev.amount() } } @@ -760,6 +789,9 @@ func (r *txRecord) setCredit(index uint32, change bool, tx *btcutil.Tx) error { // spendable by wallet. The output is added unspent, and is marked spent // when a new transaction spending the output is inserted into the store. func (t *TxRecord) AddCredit(index uint32, change bool) (Credit, error) { + t.s.mtx.Lock() + defer t.s.mtx.Unlock() + if len(t.tx.MsgTx().TxOut) <= int(index) { return Credit{}, errors.New("transaction output does not exist") } @@ -794,12 +826,17 @@ func (t *TxRecord) AddCredit(index uint32, change bool) (Credit, error) { } } - return Credit{t, index}, nil + c := Credit{t, index} + t.s.notifyNewCredit(c) + return c, nil } // Rollback removes all blocks at height onwards, moving any transactions within // each block to the unconfirmed pool. func (s *Store) Rollback(height int32) error { + s.mtx.Lock() + defer s.mtx.Unlock() + i := len(s.blocks) for i != 0 && s.blocks[i-1].Height >= height { i-- @@ -944,6 +981,9 @@ func (r *txRecord) swapDebits(previous, current BlockOutputKey) error { // transactions which debit from previous outputs and are not known to have // been mined in a block. func (s *Store) UnminedDebitTxs() []*btcutil.Tx { + s.mtx.RLock() + defer s.mtx.RUnlock() + unmined := make([]*btcutil.Tx, 0, len(s.unconfirmed.txs)) for _, r := range s.unconfirmed.spentBlockOutPoints { unmined = append(unmined, r.Tx()) @@ -1042,6 +1082,13 @@ func (s *Store) removeConflict(r *txRecord) error { // UnspentOutputs returns all unspent received transaction outputs. // The order is undefined. func (s *Store) UnspentOutputs() ([]Credit, error) { + s.mtx.RLock() + defer s.mtx.RUnlock() + + return s.unspentOutputs() +} + +func (s *Store) unspentOutputs() ([]Credit, error) { type createdCredit struct { credit Credit err error @@ -1139,7 +1186,10 @@ func (s creditSlice) Swap(i, j int) { // index in increasing order. Credits (outputs) from the same transaction // are sorted by output index in increasing order. func (s *Store) SortedUnspentOutputs() ([]Credit, error) { - unspent, err := s.UnspentOutputs() + s.mtx.RLock() + defer s.mtx.RUnlock() + + unspent, err := s.unspentOutputs() if err != nil { return []Credit{}, err } @@ -1170,6 +1220,13 @@ func confirms(txHeight, curHeight int32) int32 { // at a current chain height of curHeight. Coinbase outputs are only included // in the balance if maturity has been reached. func (s *Store) Balance(minConf int, chainHeight int32) (btcutil.Amount, error) { + s.mtx.RLock() + defer s.mtx.RUnlock() + + return s.balance(minConf, chainHeight) +} + +func (s *Store) balance(minConf int, chainHeight int32) (btcutil.Amount, error) { var bal btcutil.Amount // Shadow these functions to avoid repeating arguments unnecesarily. @@ -1244,6 +1301,9 @@ func (s *Store) Balance(minConf int, chainHeight int32) (btcutil.Amount, error) // saved by the store. This is sorted first by block height in increasing // order, and then by transaction index for each tx in a block. func (s *Store) Records() (records []*TxRecord) { + s.mtx.RLock() + defer s.mtx.RUnlock() + for _, b := range s.blocks { for _, r := range b.txs { key := BlockTxKey{r.tx.Index(), b.Block.Height} @@ -1275,6 +1335,9 @@ func (r byReceiveDate) Swap(i, j int) { r[i], r[j] = r[j], r[i] } // Debits returns the debit record for the transaction, or a non-nil error if // the transaction does not debit from any previous transaction credits. func (t *TxRecord) Debits() (Debits, error) { + t.s.mtx.RLock() + defer t.s.mtx.RUnlock() + if t.debits == nil { return Debits{}, errors.New("no debits") } @@ -1284,6 +1347,9 @@ func (t *TxRecord) Debits() (Debits, error) { // Credits returns all credit records for this transaction's outputs that are or // were spendable by wallet. func (t *TxRecord) Credits() []Credit { + t.s.mtx.RLock() + defer t.s.mtx.RUnlock() + credits := make([]Credit, 0, len(t.credits)) for i, c := range t.credits { if c != nil { @@ -1296,6 +1362,9 @@ func (t *TxRecord) Credits() []Credit { // HasCredit returns whether the transaction output at the passed index is // a wallet credit. func (t *TxRecord) HasCredit(i int) bool { + t.s.mtx.RLock() + defer t.s.mtx.RUnlock() + if len(t.credits) <= i { return false } @@ -1304,11 +1373,21 @@ func (t *TxRecord) HasCredit(i int) bool { // InputAmount returns the total amount debited from previous credits. func (d Debits) InputAmount() btcutil.Amount { + d.s.mtx.RLock() + defer d.s.mtx.RUnlock() + return d.txRecord.debits.amount } // OutputAmount returns the total amount of all outputs for a transaction. func (t *TxRecord) OutputAmount(ignoreChange bool) btcutil.Amount { + t.s.mtx.RLock() + defer t.s.mtx.RUnlock() + + return t.outputAmount(ignoreChange) +} + +func (t *TxRecord) outputAmount(ignoreChange bool) btcutil.Amount { a := btcutil.Amount(0) for i, txOut := range t.Tx().MsgTx().TxOut { if ignoreChange { @@ -1325,7 +1404,7 @@ func (t *TxRecord) OutputAmount(ignoreChange bool) btcutil.Amount { // Fee returns the difference between the debited amount and the total // transaction output. func (d Debits) Fee() btcutil.Amount { - return d.InputAmount() - d.OutputAmount(false) + return d.txRecord.debits.amount - d.outputAmount(false) } // Addresses parses the pubkey script, extracting all addresses for a @@ -1333,6 +1412,9 @@ func (d Debits) Fee() btcutil.Amount { func (c Credit) Addresses(net *btcnet.Params) (btcscript.ScriptClass, []btcutil.Address, int, error) { + c.s.mtx.RLock() + defer c.s.mtx.RUnlock() + msgTx := c.Tx().MsgTx() pkScript := msgTx.TxOut[c.OutputIndex].PkScript return btcscript.ExtractPkScriptAddrs(pkScript, net) @@ -1340,28 +1422,51 @@ func (c Credit) Addresses(net *btcnet.Params) (btcscript.ScriptClass, // Change returns whether the credit is the result of a change output. func (c Credit) Change() bool { + c.s.mtx.RLock() + defer c.s.mtx.RUnlock() + return c.txRecord.credits[c.OutputIndex].change } // Confirmed returns whether a transaction has reached some target number of // confirmations, given the current best chain height. func (t *TxRecord) Confirmed(target int, chainHeight int32) bool { + t.s.mtx.RLock() + defer t.s.mtx.RUnlock() + return confirmed(target, t.BlockHeight, chainHeight) } // Confirmations returns the total number of confirmations a transaction has // reached, given the current best chain height. func (t *TxRecord) Confirmations(chainHeight int32) int32 { + t.s.mtx.RLock() + defer t.s.mtx.RUnlock() + return confirms(t.BlockHeight, chainHeight) } // IsCoinbase returns whether the transaction is a coinbase. func (t *TxRecord) IsCoinbase() bool { + t.s.mtx.RLock() + defer t.s.mtx.RUnlock() + + return t.isCoinbase() +} + +func (t *TxRecord) isCoinbase() bool { return t.BlockHeight != -1 && t.BlockIndex == 0 } // Amount returns the amount credited to the account from a transaction output. func (c Credit) Amount() btcutil.Amount { + c.s.mtx.RLock() + defer c.s.mtx.RUnlock() + + return c.amount() +} + +func (c Credit) amount() btcutil.Amount { msgTx := c.Tx().MsgTx() return btcutil.Amount(msgTx.TxOut[c.OutputIndex].Value) } @@ -1369,6 +1474,13 @@ func (c Credit) Amount() btcutil.Amount { // OutPoint returns the outpoint needed to include in a transaction input // to spend this output. func (c Credit) OutPoint() *btcwire.OutPoint { + c.s.mtx.RLock() + defer c.s.mtx.RUnlock() + + return c.outPoint() +} + +func (c Credit) outPoint() *btcwire.OutPoint { return btcwire.NewOutPoint(c.Tx().Sha(), c.OutputIndex) } @@ -1382,11 +1494,17 @@ func (c Credit) outputKey() BlockOutputKey { // Spent returns whether the transaction output is currently spent or not. func (c Credit) Spent() bool { + c.s.mtx.RLock() + defer c.s.mtx.RUnlock() + return c.txRecord.credits[c.OutputIndex].spentBy != nil } // TxOut returns the transaction output which this credit references. func (c Credit) TxOut() *btcwire.TxOut { + c.s.mtx.RLock() + defer c.s.mtx.RUnlock() + return c.Tx().MsgTx().TxOut[c.OutputIndex] } diff --git a/txstore/tx_test.go b/txstore/tx_test.go index cc4ede5..40dd0dc 100644 --- a/txstore/tx_test.go +++ b/txstore/tx_test.go @@ -88,7 +88,7 @@ func TestInsertsCreditsDebitsRollbacks(t *testing.T) { { name: "new store", f: func(_ *Store) (*Store, error) { - return New(), nil + return New("/tmp/tx.bin"), nil }, bal: 0, unc: 0, @@ -249,7 +249,7 @@ func TestInsertsCreditsDebitsRollbacks(t *testing.T) { { name: "insert unconfirmed debit", f: func(s *Store) (*Store, error) { - prev, err := s.InsertTx(TstDoubleSpendTx, TstRecvTxBlockDetails) + _, err := s.InsertTx(TstDoubleSpendTx, TstRecvTxBlockDetails) if err != nil { return nil, err } @@ -259,7 +259,7 @@ func TestInsertsCreditsDebitsRollbacks(t *testing.T) { return nil, err } - _, err = r.AddDebits(prev.Credits()) + _, err = r.AddDebits() if err != nil { return nil, err } @@ -280,7 +280,7 @@ func TestInsertsCreditsDebitsRollbacks(t *testing.T) { { name: "insert unconfirmed debit again", f: func(s *Store) (*Store, error) { - prev, err := s.InsertTx(TstDoubleSpendTx, TstRecvTxBlockDetails) + _, err := s.InsertTx(TstDoubleSpendTx, TstRecvTxBlockDetails) if err != nil { return nil, err } @@ -290,7 +290,7 @@ func TestInsertsCreditsDebitsRollbacks(t *testing.T) { return nil, err } - _, err = r.AddDebits(prev.Credits()) + _, err = r.AddDebits() if err != nil { return nil, err } @@ -548,7 +548,7 @@ func TestInsertsCreditsDebitsRollbacks(t *testing.T) { } func TestFindingSpentCredits(t *testing.T) { - s := New() + s := New("/tmp/tx.bin") // Insert transaction and credit which will be spent. r, err := s.InsertTx(TstRecvTx, TstRecvTxBlockDetails) @@ -570,7 +570,7 @@ func TestFindingSpentCredits(t *testing.T) { if err != nil { t.Fatal(err) } - _, err = r2.AddDebits(nil) + _, err = r2.AddDebits() if err != nil { t.Fatal(err) } diff --git a/updates.go b/updates.go deleted file mode 100644 index 87dc984..0000000 --- a/updates.go +++ /dev/null @@ -1,166 +0,0 @@ -/* - * Copyright (c) 2013, 2014 Conformal Systems LLC - * - * Permission to use, copy, modify, and distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - */ - -package main - -import ( - "errors" - "os" - "path/filepath" - "strings" -) - -// ErrNotAccountDir describes an error where a directory in the btcwallet -// data directory cannot be parsed as a directory holding account files. -var ErrNotAccountDir = errors.New("directory is not an account directory") - -// updateOldFileLocations moves files for wallets, transactions, and -// recorded unspent transaction outputs to more recent locations. -func updateOldFileLocations() error { - // Before version 0.1.1, accounts were saved with the following - // format: - // - // ~/.btcwallet/ - // - btcwallet/ - // - wallet.bin - // - tx.bin - // - utxo.bin - // - btcwallet-AccountA/ - // - wallet.bin - // - tx.bin - // - utxo.bin - // - // This format does not scale well (see Github issue #16), and - // since version 0.1.1, the above directory format has changed - // to the following: - // - // ~/.btcwallet/ - // - testnet/ - // - wallet.bin - // - tx.bin - // - utxo.bin - // - AccountA-wallet.bin - // - AccountA-tx.bin - // - AccountA-utxo.bin - // - // Previous account files are placed in the testnet directory - // as 0.1.0 and earlier only ran on testnet. - // - // UTXOs and transaction history are intentionally not moved over, as - // the UTXO file is no longer used (it was combined with txstore), and - // the tx history is now written in an incompatible format and would - // be ignored on first read. - - datafi, err := os.Open(cfg.DataDir) - if err != nil { - return nil - } - defer func() { - if err := datafi.Close(); err != nil { - log.Warnf("Cannot close data directory: %v", err) - } - }() - - // Get info on all files in the data directory. - fi, err := datafi.Readdir(0) - if err != nil { - log.Errorf("Cannot read files in data directory: %v", err) - return err - } - - acctsExist := false - for i := range fi { - // Ignore non-directories. - if !fi[i].IsDir() { - continue - } - - if strings.HasPrefix(fi[i].Name(), "btcwallet") { - acctsExist = true - break - } - } - if !acctsExist { - return nil - } - - // Create testnet directory, if it doesn't already exist. - netdir := filepath.Join(cfg.DataDir, "testnet") - if err := checkCreateDir(netdir); err != nil { - log.Errorf("Cannot continue without a testnet directory: %v", err) - return err - } - - // Check all files in the datadir for old accounts to update. - for i := range fi { - // Ignore non-directories. - if !fi[i].IsDir() { - continue - } - - account, err := parseOldAccountDir(cfg.DataDir, fi[i].Name()) - switch err { - case nil: - break - - case ErrNotAccountDir: - continue - - default: // all other non-nil errors - log.Errorf("Cannot open old account directory: %v", err) - return err - } - - log.Infof("Updating old file locations for account %v", account) - - // Move old wallet.bin, if any. - old := filepath.Join(cfg.DataDir, fi[i].Name(), "wallet.bin") - if fileExists(old) { - new := accountFilename("wallet.bin", account, netdir) - if err := Rename(old, new); err != nil { - log.Errorf("Cannot move old %v for account %v to new location: %v", - "wallet.bin", account, err) - return err - } - } - - // Cleanup old account directory. - if err := os.RemoveAll(filepath.Join(cfg.DataDir, fi[i].Name())); err != nil { - log.Warnf("Could not remove pre 0.1.1 account directory: %v", err) - } - } - - return nil -} - -type oldAccountDir struct { - account string - dir *os.File -} - -func parseOldAccountDir(dir, base string) (string, error) { - if base == "btcwallet" { - return "", nil - } - - const accountPrefix = "btcwallet-" - if strings.HasPrefix(base, accountPrefix) { - account := strings.TrimPrefix(base, accountPrefix) - return account, nil - } - - return "", ErrNotAccountDir -} diff --git a/wallet.go b/wallet.go new file mode 100644 index 0000000..3995959 --- /dev/null +++ b/wallet.go @@ -0,0 +1,1367 @@ +/* + * Copyright (c) 2013, 2014 Conformal Systems LLC + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +package main + +import ( + "bytes" + "encoding/base64" + "encoding/hex" + "errors" + "fmt" + "path/filepath" + "sync" + "time" + + "github.com/conformal/btcchain" + "github.com/conformal/btcjson" + "github.com/conformal/btcnet" + "github.com/conformal/btcutil" + "github.com/conformal/btcwallet/chain" + "github.com/conformal/btcwallet/keystore" + "github.com/conformal/btcwallet/txstore" + "github.com/conformal/btcwire" +) + +var ( + ErrNoWalletFiles = errors.New("no wallet files") + + ErrWalletExists = errors.New("wallet already exists") + + ErrNotSynced = errors.New("wallet is not synchronized with the chain server") +) + +// networkDir returns the directory name of a network directory to hold wallet +// files. +func networkDir(net *btcnet.Params) string { + netname := net.Name + + // For now, we must always name the testnet data directory as "testnet" + // and not "testnet3" or any other version, as the btcnet testnet3 + // paramaters will likely be switched to being named "testnet3" in the + // future. This is done to future proof that change, and an upgrade + // plan to move the testnet3 data directory can be worked out later. + if net.Net == btcwire.TestNet3 { + netname = "testnet" + } + + return filepath.Join(cfg.DataDir, netname) +} + +// Wallet is a structure containing all the components for a +// complete wallet. It contains the Armory-style key store +// addresses and keys), +type Wallet struct { + // Data stores + KeyStore *keystore.Store + TxStore *txstore.Store + + chainSvr *chain.Client + chainSvrLock sync.Locker + chainSynced chan struct{} // closed when synced + + lockedOutpoints map[btcwire.OutPoint]struct{} + FeeIncrement btcutil.Amount + + // Channels for rescan processing. Requests are added and merged with + // any waiting requests, before being sent to another goroutine to + // call the rescan RPC. + rescanAddJob chan *RescanJob + rescanBatch chan *rescanBatch + rescanNotifications chan interface{} // From chain server + rescanProgress chan *RescanProgressMsg + rescanFinished chan *RescanFinishedMsg + + // Channels for the keystore locker. + unlockRequests chan unlockRequest + lockRequests chan struct{} + holdUnlockRequests chan chan HeldUnlock + lockState chan bool + changePassphrase chan changePassphraseRequest + + // Notification channels so other components can listen in on wallet + // activity. These are initialized as nil, and must be created by + // calling one of the Listen* methods. + connectedBlocks chan keystore.BlockStamp + disconnectedBlocks chan keystore.BlockStamp + lockStateChanges chan bool // true when locked + confirmedBalance chan btcutil.Amount + unconfirmedBalance chan btcutil.Amount + chainServerConnected chan bool + notificationLock sync.Locker + + wg sync.WaitGroup + quit chan struct{} +} + +// newWallet creates a new Wallet structure with the provided key and +// transaction stores. +func newWallet(keys *keystore.Store, txs *txstore.Store) *Wallet { + return &Wallet{ + KeyStore: keys, + TxStore: txs, + chainSvrLock: new(sync.Mutex), + chainSynced: make(chan struct{}), + lockedOutpoints: map[btcwire.OutPoint]struct{}{}, + FeeIncrement: defaultFeeIncrement, + rescanAddJob: make(chan *RescanJob), + rescanBatch: make(chan *rescanBatch), + rescanNotifications: make(chan interface{}), + rescanProgress: make(chan *RescanProgressMsg), + rescanFinished: make(chan *RescanFinishedMsg), + unlockRequests: make(chan unlockRequest), + lockRequests: make(chan struct{}), + holdUnlockRequests: make(chan chan HeldUnlock), + lockState: make(chan bool), + changePassphrase: make(chan changePassphraseRequest), + notificationLock: new(sync.Mutex), + quit: make(chan struct{}), + } +} + +// ErrDuplicateListen is returned for any attempts to listen for the same +// notification more than once. If callers must pass along a notifiation to +// multiple places, they must broadcast it themself. +var ErrDuplicateListen = errors.New("duplicate listen") + +func (w *Wallet) updateNotificationLock() { + switch { + case w.connectedBlocks == nil: + fallthrough + case w.disconnectedBlocks == nil: + fallthrough + case w.lockStateChanges == nil: + fallthrough + case w.confirmedBalance == nil: + fallthrough + case w.unconfirmedBalance == nil: + fallthrough + case w.chainServerConnected == nil: + return + } + w.notificationLock = noopLocker{} +} + +// ListenConnectedBlocks returns a channel that passes all blocks that a wallet +// has been marked in sync with. The channel must be read, or other wallet +// methods will block. +// +// If this is called twice, ErrDuplicateListen is returned. +func (w *Wallet) ListenConnectedBlocks() (<-chan keystore.BlockStamp, error) { + w.notificationLock.Lock() + defer w.notificationLock.Unlock() + + if w.connectedBlocks != nil { + return nil, ErrDuplicateListen + } + w.connectedBlocks = make(chan keystore.BlockStamp) + w.updateNotificationLock() + return w.connectedBlocks, nil +} + +// ListenDisconnectedBlocks returns a channel that passes all blocks that a +// wallet has detached. The channel must be read, or other wallet methods will +// block. +// +// If this is called twice, ErrDuplicateListen is returned. +func (w *Wallet) ListenDisconnectedBlocks() (<-chan keystore.BlockStamp, error) { + w.notificationLock.Lock() + defer w.notificationLock.Unlock() + + if w.disconnectedBlocks != nil { + return nil, ErrDuplicateListen + } + w.disconnectedBlocks = make(chan keystore.BlockStamp) + w.updateNotificationLock() + return w.disconnectedBlocks, nil +} + +// ListenDisconnectedBlocks returns a channel that passes the current lock state +// of the wallet keystore anytime the keystore is locked or unlocked. The value +// is true for locked, and false for unlocked. The channel must be read, or +// other wallet methods will block. +// +// If this is called twice, ErrDuplicateListen is returned. +func (w *Wallet) ListenKeystoreLockStatus() (<-chan bool, error) { + w.notificationLock.Lock() + defer w.notificationLock.Unlock() + + if w.lockStateChanges != nil { + return nil, ErrDuplicateListen + } + w.lockStateChanges = make(chan bool) + w.updateNotificationLock() + return w.lockStateChanges, nil +} + +// ListenConfirmedBalance returns a channel that passes the confirmed balance +// when any changes to the balance are made. This channel must be read, or +// other wallet methods will block. +// +// If this is called twice, ErrDuplicateListen is returned. +func (w *Wallet) ListenConfirmedBalance() (<-chan btcutil.Amount, error) { + w.notificationLock.Lock() + defer w.notificationLock.Unlock() + + if w.confirmedBalance != nil { + return nil, ErrDuplicateListen + } + w.confirmedBalance = make(chan btcutil.Amount) + w.updateNotificationLock() + return w.confirmedBalance, nil +} + +// ListenUnconfirmedBalance returns a channel that passes the unconfirmed +// balance when any changes to the balance are made. This channel must be +// read, or other wallet methods will block. +// +// If this is called twice, ErrDuplicateListen is returned. +func (w *Wallet) ListenUnconfirmedBalance() (<-chan btcutil.Amount, error) { + w.notificationLock.Lock() + defer w.notificationLock.Unlock() + + if w.unconfirmedBalance != nil { + return nil, ErrDuplicateListen + } + w.unconfirmedBalance = make(chan btcutil.Amount) + w.updateNotificationLock() + return w.unconfirmedBalance, nil +} + +func (w *Wallet) ListenChainServerConnected() (<-chan bool, error) { + w.notificationLock.Lock() + defer w.notificationLock.Unlock() + + if w.chainServerConnected != nil { + return nil, ErrDuplicateListen + } + w.chainServerConnected = make(chan bool) + w.updateNotificationLock() + return w.chainServerConnected, nil +} + +func (w *Wallet) notifyConnectedBlock(block keystore.BlockStamp) { + w.notificationLock.Lock() + if w.connectedBlocks != nil { + w.connectedBlocks <- block + } + w.notificationLock.Unlock() +} + +func (w *Wallet) notifyDisconnectedBlock(block keystore.BlockStamp) { + w.notificationLock.Lock() + if w.disconnectedBlocks != nil { + w.disconnectedBlocks <- block + } + w.notificationLock.Unlock() +} + +func (w *Wallet) notifyLockStateChange(locked bool) { + w.notificationLock.Lock() + if w.lockStateChanges != nil { + w.lockStateChanges <- locked + } + w.notificationLock.Unlock() +} + +func (w *Wallet) notifyConfirmedBalance(bal btcutil.Amount) { + w.notificationLock.Lock() + if w.confirmedBalance != nil { + w.confirmedBalance <- bal + } + w.notificationLock.Unlock() +} + +func (w *Wallet) notifyUnconfirmedBalance(bal btcutil.Amount) { + w.notificationLock.Lock() + if w.unconfirmedBalance != nil { + w.unconfirmedBalance <- bal + } + w.notificationLock.Unlock() +} + +func (w *Wallet) notifyChainServerConnected(connected bool) { + w.notificationLock.Lock() + if w.chainServerConnected != nil { + w.chainServerConnected <- connected + } + w.notificationLock.Unlock() +} + +// openWallet opens a new wallet from disk. +func openWallet() (*Wallet, error) { + netdir := networkDir(activeNet.Params) + + // Ensure that the network directory exists. + // TODO: move this? + if err := checkCreateDir(netdir); err != nil { + return nil, err + } + + // Read key and transaction stores. + keys, err := keystore.OpenDir(netdir) + var txs *txstore.Store + if err == nil { + txs, err = txstore.OpenDir(netdir) + } + if err != nil { + // Special case: if the keystore was successfully read + // (keys != nil) but the transaction store was not, create a + // new txstore and write it out to disk. Write an unsynced + // wallet back to disk so on future opens, the empty txstore + // is not considered fully synced. + if keys == nil { + return nil, err + } + + txs = txstore.New(netdir) + txs.MarkDirty() + err = txs.WriteIfDirty() + if err != nil { + return nil, err + } + keys.SetSyncedWith(nil) + keys.MarkDirty() + err = keys.WriteIfDirty() + if err != nil { + return nil, err + } + } + + log.Infof("Opened wallet files") // TODO: log balance? last sync height? + return newWallet(keys, txs), nil +} + +// newEncryptedWallet creates a new wallet encrypted with the provided +// passphrase. +func newEncryptedWallet(passphrase []byte, chainSvr *chain.Client) (*Wallet, error) { + // Get current block's height and hash. + bs, err := chainSvr.BlockStamp() + if err != nil { + return nil, err + } + + // Create new wallet in memory. + keys, err := keystore.New(networkDir(activeNet.Params), "Default acccount", + passphrase, activeNet.Params, bs) + if err != nil { + return nil, err + } + + w := newWallet(keys, txstore.New(networkDir(activeNet.Params))) + return w, nil +} + +// Start starts the goroutines necessary to manage a wallet. +func (w *Wallet) Start(chainServer *chain.Client) { + select { + case <-w.quit: + return + default: + } + + w.chainSvrLock.Lock() + defer w.chainSvrLock.Unlock() + + w.notifyChainServerConnected(!chainServer.Disconnected()) + + w.chainSvr = chainServer + w.chainSvrLock = noopLocker{} + + w.wg.Add(6) + go w.diskWriter() + go w.handleChainNotifications() + go w.keystoreLocker() + go w.rescanBatchHandler() + go w.rescanProgressHandler() + go w.rescanRPCHandler() + + go func() { + err := w.syncWithChain() + if err != nil && !w.ShuttingDown() { + log.Warnf("Unable to synchronize wallet to chain: %v", err) + } + }() +} + +// Stop signals all wallet goroutines to shutdown. +func (w *Wallet) Stop() { + select { + case <-w.quit: + default: + close(w.quit) + w.chainSvrLock.Lock() + if w.chainSvr != nil { + w.chainSvr.Stop() + } + w.chainSvrLock.Unlock() + } +} + +// ShuttingDown returns whether the wallet is currently in the process of +// shutting down or not. +func (w *Wallet) ShuttingDown() bool { + select { + case <-w.quit: + return true + default: + return false + } +} + +// WaitForShutdown blocks until all wallet goroutines have finished executing. +func (w *Wallet) WaitForShutdown() { + w.chainSvrLock.Lock() + if w.chainSvr != nil { + w.chainSvr.WaitForShutdown() + } + w.chainSvrLock.Unlock() + w.wg.Wait() +} + +// ChainSynced returns whether the wallet has been attached to a chain server +// and synced up to the best block on the main chain. +func (w *Wallet) ChainSynced() bool { + select { + case <-w.chainSynced: + return true + default: + return false + } +} + +// WaitForChainSync blocks until a wallet has been synced with the main chain +// of an attached chain server. +func (w *Wallet) WaitForChainSync() { + <-w.chainSynced +} + +// SynchedChainTip returns the hash and height of the block of the most +// recently seen block in the main chain. It returns errors if the +// wallet has not yet been marked as synched with the chain. +func (w *Wallet) SyncedChainTip() (*keystore.BlockStamp, error) { + select { + case <-w.chainSynced: + return w.chainSvr.BlockStamp() + default: + return nil, ErrNotSynced + } +} + +func (w *Wallet) syncWithChain() (err error) { + defer func() { + if err == nil { + // Request notifications for connected and disconnected + // blocks. + err = w.chainSvr.NotifyBlocks() + } + }() + + // TODO(jrick): How should this handle a synced height earlier than + // the chain server best block? + + // Check that there was not any reorgs done since last connection. + // If so, rollback and rescan to catch up. + iter := w.KeyStore.NewIterateRecentBlocks() + for cont := iter != nil; cont; cont = iter.Prev() { + bs := iter.BlockStamp() + log.Debugf("Checking for previous saved block with height %v hash %v", + bs.Height, bs.Hash) + + if _, err := w.chainSvr.GetBlock(bs.Hash); err != nil { + continue + } + + log.Debug("Found matching block.") + + // If we had to go back to any previous blocks (iter.Next + // returns true), then rollback the next and all child blocks. + if iter.Next() { + bs := iter.BlockStamp() + w.KeyStore.SetSyncedWith(&bs) + err = w.TxStore.Rollback(bs.Height) + if err != nil { + return + } + w.TxStore.MarkDirty() + } + + break + } + + return w.RescanActiveAddresses() +} + +type ( + unlockRequest struct { + passphrase []byte + timeout time.Duration // Zero value prevents the timeout. + err chan error + } + + changePassphraseRequest struct { + old, new []byte + err chan error + } + + HeldUnlock chan struct{} +) + +// keystoreLocker manages the locked/unlocked state of a wallet. +func (w *Wallet) keystoreLocker() { + var timeout <-chan time.Time + holdChan := make(HeldUnlock) +out: + for { + select { + case req := <-w.unlockRequests: + err := w.KeyStore.Unlock(req.passphrase) + if err != nil { + req.err <- err + continue + } + w.notifyLockStateChange(false) + if req.timeout == 0 { + timeout = nil + } else { + timeout = time.After(req.timeout) + } + req.err <- nil + continue + + case req := <-w.changePassphrase: + // Changing the passphrase requires an unlocked + // keystore, and for the old passphrase to be correct. + // Lock the keystore and unlock with the old passphase + // check its validity. + _ = w.KeyStore.Lock() + w.notifyLockStateChange(true) + timeout = nil + err := w.KeyStore.Unlock(req.old) + if err == nil { + w.notifyLockStateChange(false) + err = w.KeyStore.ChangePassphrase(req.new) + } + req.err <- err + continue + + case req := <-w.holdUnlockRequests: + if w.KeyStore.IsLocked() { + close(req) + continue + } + + req <- holdChan + <-holdChan // Block until the lock is released. + + // If, after holding onto the unlocked wallet for some + // time, the timeout has expired, lock it now instead + // of hoping it gets unlocked next time the top level + // select runs. + select { + case <-timeout: + // Let the top level select fallthrough so the + // wallet is locked. + default: + continue + } + + case w.lockState <- w.KeyStore.IsLocked(): + continue + + case <-w.quit: + break out + + case <-w.lockRequests: + case <-timeout: + } + + // Select statement fell through by an explicit lock or the + // timer expiring. Lock the keystores here. + timeout = nil + if err := w.KeyStore.Lock(); err != nil { + log.Errorf("Could not lock wallet: %v", err) + } + w.notifyLockStateChange(true) + } + w.wg.Done() +} + +// Unlock unlocks the wallet's keystore and locks the wallet again after +// timeout has expired. If the wallet is already unlocked and the new +// passphrase is correct, the current timeout is replaced with the new one. +func (w *Wallet) Unlock(passphrase []byte, timeout time.Duration) error { + err := make(chan error, 1) + w.unlockRequests <- unlockRequest{ + passphrase: passphrase, + timeout: timeout, + err: err, + } + return <-err +} + +// Lock locks the wallet's keystore. +func (w *Wallet) Lock() { + w.lockRequests <- struct{}{} +} + +// Locked returns whether the keystore for a wallet is locked. +func (w *Wallet) Locked() bool { + return <-w.lockState +} + +// HoldUnlock prevents the wallet from being locked, +func (w *Wallet) HoldUnlock() (HeldUnlock, error) { + req := make(chan HeldUnlock) + w.holdUnlockRequests <- req + hl, ok := <-req + if !ok { + return nil, keystore.ErrLocked + } + return hl, nil +} + +// Release releases the hold on the unlocked-state of the wallet and allows the +// wallet to be locked again. If a lock timeout has already expired, the +// wallet is locked again as soon as Release is called. +func (c HeldUnlock) Release() { + c <- struct{}{} +} + +// ChangePassphrase attempts to change the passphrase for a wallet from old +// to new. Changing the passphrase is synchronized with all other keystore +// locking and unlocking, and will result in a locked wallet on success. +func (w *Wallet) ChangePassphrase(old, new []byte) error { + err := make(chan error, 1) + w.changePassphrase <- changePassphraseRequest{ + old: old, + new: new, + err: err, + } + return <-err +} + +// diskWriter periodically (every 10 seconds) writes out the key and transaction +// stores to disk if they are marked dirty. On shutdown, +func (w *Wallet) diskWriter() { + ticker := time.NewTicker(10 * time.Second) + var wg sync.WaitGroup + var done bool + + for { + select { + case <-ticker.C: + case <-w.quit: + done = true + } + + log.Trace("Writing wallet files") + + wg.Add(2) + go func() { + err := w.KeyStore.WriteIfDirty() + if err != nil { + log.Errorf("Cannot write keystore: %v", + err) + } + wg.Done() + }() + go func() { + err := w.TxStore.WriteIfDirty() + if err != nil { + log.Errorf("Cannot write txstore: %v", + err) + } + wg.Done() + }() + wg.Wait() + + if done { + break + } + } + w.wg.Done() +} + +// AddressUsed returns whether there are any recorded transactions spending to +// a given address. Assumming correct TxStore usage, this will return true iff +// there are any transactions with outputs to this address in the blockchain or +// the btcd mempool. +func (w *Wallet) AddressUsed(addr btcutil.Address) bool { + // This not only can be optimized by recording this data as it is + // read when opening a wallet, and keeping it up to date each time a + // new received tx arrives, but it probably should in case an address is + // used in a tx (made public) but the tx is eventually removed from the + // store (consider a chain reorg). + + for _, r := range w.TxStore.Records() { + for _, c := range r.Credits() { + // Errors don't matter here. If addrs is nil, the + // range below does nothing. + _, addrs, _, _ := c.Addresses(activeNet.Params) + for _, a := range addrs { + if addr.String() == a.String() { + return true + } + } + } + } + return false +} + +// CalculateBalance sums the amounts of all unspent transaction +// outputs to addresses of a wallet and returns the balance. +// +// If confirmations is 0, all UTXOs, even those not present in a +// block (height -1), will be used to get the balance. Otherwise, +// a UTXO must be in a block. If confirmations is 1 or greater, +// the balance will be calculated based on how many how many blocks +// include a UTXO. +func (w *Wallet) CalculateBalance(confirms int) (btcutil.Amount, error) { + bs, err := w.SyncedChainTip() + if err != nil { + return 0, err + } + + return w.TxStore.Balance(confirms, bs.Height) +} + +// CalculateAddressBalance sums the amounts of all unspent transaction +// outputs to a single address's pubkey hash and returns the balance. +// +// If confirmations is 0, all UTXOs, even those not present in a +// block (height -1), will be used to get the balance. Otherwise, +// a UTXO must be in a block. If confirmations is 1 or greater, +// the balance will be calculated based on how many how many blocks +// include a UTXO. +func (w *Wallet) CalculateAddressBalance(addr btcutil.Address, confirms int) (btcutil.Amount, error) { + bs, err := w.SyncedChainTip() + if err != nil { + return 0, err + } + + var bal btcutil.Amount + unspent, err := w.TxStore.UnspentOutputs() + if err != nil { + return 0, err + } + for _, credit := range unspent { + if credit.Confirmed(confirms, bs.Height) { + // We only care about the case where len(addrs) == 1, and err + // will never be non-nil in that case + _, addrs, _, _ := credit.Addresses(activeNet.Params) + if len(addrs) != 1 { + continue + } + if addrs[0].EncodeAddress() == addr.EncodeAddress() { + bal += credit.Amount() + } + } + } + return bal, nil +} + +// CurrentAddress gets the most recently requested Bitcoin payment address +// from a wallet. If the address has already been used (there is at least +// one transaction spending to it in the blockchain or btcd mempool), the next +// chained address is returned. +func (w *Wallet) CurrentAddress() (btcutil.Address, error) { + addr := w.KeyStore.LastChainedAddress() + + // Get next chained address if the last one has already been used. + if w.AddressUsed(addr) { + return w.NewAddress() + } + + return addr, nil +} + +// ListSinceBlock returns a slice of objects with details about transactions +// since the given block. If the block is -1 then all transactions are included. +// This is intended to be used for listsinceblock RPC replies. +func (w *Wallet) ListSinceBlock(since, curBlockHeight int32, + minconf int) ([]btcjson.ListTransactionsResult, error) { + + txList := []btcjson.ListTransactionsResult{} + for _, txRecord := range w.TxStore.Records() { + // Transaction records must only be considered if they occur + // after the block height since. + if since != -1 && txRecord.BlockHeight <= since { + continue + } + + // Transactions that have not met minconf confirmations are to + // be ignored. + if !txRecord.Confirmed(minconf, curBlockHeight) { + continue + } + + jsonResults, err := txRecord.ToJSON("", curBlockHeight, + w.KeyStore.Net()) + if err != nil { + return nil, err + } + txList = append(txList, jsonResults...) + } + + return txList, nil +} + +// ListTransactions returns a slice of objects with details about a recorded +// transaction. This is intended to be used for listtransactions RPC +// replies. +func (w *Wallet) ListTransactions(from, count int) ([]btcjson.ListTransactionsResult, error) { + txList := []btcjson.ListTransactionsResult{} + + // Get current block. The block height used for calculating + // the number of tx confirmations. + bs, err := w.SyncedChainTip() + if err != nil { + return txList, err + } + + records := w.TxStore.Records() + lastLookupIdx := len(records) - count + // Search in reverse order: lookup most recently-added first. + for i := len(records) - 1; i >= from && i >= lastLookupIdx; i-- { + jsonResults, err := records[i].ToJSON("", bs.Height, + w.KeyStore.Net()) + if err != nil { + return nil, err + } + txList = append(txList, jsonResults...) + } + + return txList, nil +} + +// ListAddressTransactions returns a slice of objects with details about +// recorded transactions to or from any address belonging to a set. This is +// intended to be used for listaddresstransactions RPC replies. +func (w *Wallet) ListAddressTransactions(pkHashes map[string]struct{}) ( + []btcjson.ListTransactionsResult, error) { + + txList := []btcjson.ListTransactionsResult{} + + // Get current block. The block height used for calculating + // the number of tx confirmations. + bs, err := w.SyncedChainTip() + if err != nil { + return txList, err + } + + for _, r := range w.TxStore.Records() { + for _, c := range r.Credits() { + // We only care about the case where len(addrs) == 1, + // and err will never be non-nil in that case. + _, addrs, _, _ := c.Addresses(activeNet.Params) + if len(addrs) != 1 { + continue + } + apkh, ok := addrs[0].(*btcutil.AddressPubKeyHash) + if !ok { + continue + } + + if _, ok := pkHashes[string(apkh.ScriptAddress())]; !ok { + continue + } + jsonResult, err := c.ToJSON("", bs.Height, + w.KeyStore.Net()) + if err != nil { + return nil, err + } + txList = append(txList, jsonResult) + } + } + + return txList, nil +} + +// ListAllTransactions returns a slice of objects with details about a recorded +// transaction. This is intended to be used for listalltransactions RPC +// replies. +func (w *Wallet) ListAllTransactions() ([]btcjson.ListTransactionsResult, error) { + txList := []btcjson.ListTransactionsResult{} + + // Get current block. The block height used for calculating + // the number of tx confirmations. + bs, err := w.SyncedChainTip() + if err != nil { + return txList, err + } + + // Search in reverse order: lookup most recently-added first. + records := w.TxStore.Records() + for i := len(records) - 1; i >= 0; i-- { + jsonResults, err := records[i].ToJSON("", bs.Height, + w.KeyStore.Net()) + if err != nil { + return nil, err + } + txList = append(txList, jsonResults...) + } + + return txList, nil +} + +// ListUnspent returns a slice of objects representing the unspent wallet +// transactions fitting the given criteria. The confirmations will be more than +// minconf, less than maxconf and if addresses is populated only the addresses +// contained within it will be considered. If we know nothing about a +// transaction an empty array will be returned. +func (w *Wallet) ListUnspent(minconf, maxconf int, + addresses map[string]bool) ([]*btcjson.ListUnspentResult, error) { + + results := []*btcjson.ListUnspentResult{} + + bs, err := w.SyncedChainTip() + if err != nil { + return results, err + } + + filter := len(addresses) != 0 + + unspent, err := w.TxStore.SortedUnspentOutputs() + if err != nil { + return nil, err + } + + for _, credit := range unspent { + confs := credit.Confirmations(bs.Height) + if int(confs) < minconf || int(confs) > maxconf { + continue + } + if credit.IsCoinbase() { + if !credit.Confirmed(btcchain.CoinbaseMaturity, bs.Height) { + continue + } + } + if w.LockedOutpoint(*credit.OutPoint()) { + continue + } + + _, addrs, _, _ := credit.Addresses(activeNet.Params) + if filter { + for _, addr := range addrs { + _, ok := addresses[addr.EncodeAddress()] + if ok { + goto include + } + } + continue + } + include: + result := &btcjson.ListUnspentResult{ + TxId: credit.Tx().Sha().String(), + Vout: credit.OutputIndex, + Account: "", + ScriptPubKey: hex.EncodeToString(credit.TxOut().PkScript), + Amount: credit.Amount().ToUnit(btcutil.AmountBTC), + Confirmations: int64(confs), + } + + // BUG: this should be a JSON array so that all + // addresses can be included, or removed (and the + // caller extracts addresses from the pkScript). + if len(addrs) > 0 { + result.Address = addrs[0].EncodeAddress() + } + + results = append(results, result) + } + + return results, nil +} + +// DumpPrivKeys returns the WIF-encoded private keys for all addresses with +// private keys in a wallet. +func (w *Wallet) DumpPrivKeys() ([]string, error) { + // Iterate over each active address, appending the private + // key to privkeys. + privkeys := []string{} + for _, info := range w.KeyStore.ActiveAddresses() { + // Only those addresses with keys needed. + pka, ok := info.(keystore.PubKeyAddress) + if !ok { + continue + } + wif, err := pka.ExportPrivKey() + if err != nil { + // It would be nice to zero out the array here. However, + // since strings in go are immutable, and we have no + // control over the caller I don't think we can. :( + return nil, err + } + privkeys = append(privkeys, wif.String()) + } + + return privkeys, nil +} + +// DumpWIFPrivateKey returns the WIF encoded private key for a +// single wallet address. +func (w *Wallet) DumpWIFPrivateKey(addr btcutil.Address) (string, error) { + // Get private key from wallet if it exists. + address, err := w.KeyStore.Address(addr) + if err != nil { + return "", err + } + + pka, ok := address.(keystore.PubKeyAddress) + if !ok { + return "", fmt.Errorf("address %s is not a key type", addr) + } + + wif, err := pka.ExportPrivKey() + if err != nil { + return "", err + } + return wif.String(), nil +} + +// ImportPrivateKey imports a private key to the wallet and writes the new +// wallet to disk. +func (w *Wallet) ImportPrivateKey(wif *btcutil.WIF, bs *keystore.BlockStamp, + rescan bool) (string, error) { + + // Attempt to import private key into wallet. + addr, err := w.KeyStore.ImportPrivateKey(wif, bs) + if err != nil { + return "", err + } + + // Immediately write wallet to disk. + w.KeyStore.MarkDirty() + if err := w.KeyStore.WriteIfDirty(); err != nil { + return "", fmt.Errorf("cannot write key: %v", err) + } + + // Rescan blockchain for transactions with txout scripts paying to the + // imported address. + if rescan { + job := &RescanJob{ + Addrs: []btcutil.Address{addr}, + OutPoints: nil, + BlockStamp: keystore.BlockStamp{ + Hash: activeNet.Params.GenesisHash, + Height: 0, + }, + } + + // 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) + } + + addrStr := addr.EncodeAddress() + log.Infof("Imported payment address %s", addrStr) + + // Return the payment address string of the imported private key. + return addrStr, nil +} + +// ExportWatchingWallet returns the watching-only copy of a wallet. Both wallets +// share the same tx store, so locking one will lock the other as well. The +// returned wallet should be serialized and exported quickly, and then dropped +// from scope. +func (w *Wallet) ExportWatchingWallet() (*Wallet, error) { + ww, err := w.KeyStore.ExportWatchingWallet() + if err != nil { + return nil, err + } + + wa := *w + wa.KeyStore = ww + return &wa, nil +} + +// exportBase64 exports a wallet's serialized key, and tx stores as +// base64-encoded values in a map. +func (w *Wallet) exportBase64() (map[string]string, error) { + buf := bytes.Buffer{} + m := make(map[string]string) + + _, err := w.KeyStore.WriteTo(&buf) + if err != nil { + return nil, err + } + m["wallet"] = base64.StdEncoding.EncodeToString(buf.Bytes()) + buf.Reset() + + if _, err = w.TxStore.WriteTo(&buf); err != nil { + return nil, err + } + m["tx"] = base64.StdEncoding.EncodeToString(buf.Bytes()) + buf.Reset() + + return m, 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 btcwire.OutPoint) bool { + _, locked := w.lockedOutpoints[op] + return locked +} + +// LockOutpoint marks an outpoint as locked, that is, it should not be used as +// an input for newly created transactions. +func (w *Wallet) LockOutpoint(op btcwire.OutPoint) { + w.lockedOutpoints[op] = struct{}{} +} + +// UnlockOutpoint marks an outpoint as unlocked, that is, it may be used as an +// input for newly created transactions. +func (w *Wallet) UnlockOutpoint(op btcwire.OutPoint) { + delete(w.lockedOutpoints, op) +} + +// ResetLockedOutpoints resets the set of locked outpoints so all may be used +// as inputs for new transactions. +func (w *Wallet) ResetLockedOutpoints() { + w.lockedOutpoints = map[btcwire.OutPoint]struct{}{} +} + +// LockedOutpoints returns a slice of currently locked outpoints. This is +// intended to be used by marshaling the result as a JSON array for +// listlockunspent RPC results. +func (w *Wallet) LockedOutpoints() []btcjson.TransactionInput { + locked := make([]btcjson.TransactionInput, len(w.lockedOutpoints)) + i := 0 + for op := range w.lockedOutpoints { + locked[i] = btcjson.TransactionInput{ + Txid: op.Hash.String(), + Vout: op.Index, + } + i++ + } + return locked +} + +// Track requests btcd to send notifications of new transactions for +// each address stored in a wallet. +func (w *Wallet) Track() { + // Request notifications for transactions sending to all wallet + // addresses. + // + // TODO: return as slice? (doesn't have to be ordered, or + // SortedActiveAddresses would be fine.) + addrMap := w.KeyStore.ActiveAddresses() + addrs := make([]btcutil.Address, len(addrMap)) + i := 0 + for addr := range addrMap { + addrs[i] = addr + i++ + } + + if err := w.chainSvr.NotifyReceived(addrs); err != nil { + log.Error("Unable to request transaction updates for address.") + } + + unspent, err := w.TxStore.UnspentOutputs() + if err != nil { + log.Errorf("Unable to access unspent outputs: %v", err) + return + } + w.ReqSpentUtxoNtfns(unspent) +} + +// ResendUnminedTxs iterates through all transactions that spend from wallet +// credits that are not known to have been mined into a block, and attempts +// to send each to the chain server for relay. +func (w *Wallet) ResendUnminedTxs() { + txs := w.TxStore.UnminedDebitTxs() + for _, tx := range txs { + _, err := w.chainSvr.SendRawTransaction(tx.MsgTx(), false) + if err != nil { + // TODO(jrick): Check error for if this tx is a double spend, + // remove it if so. + log.Debugf("Could not resend transaction %v: %v", + tx.Sha(), err) + continue + } + log.Debugf("Resent unmined transaction %v", tx.Sha()) + } +} + +// SortedActivePaymentAddresses returns a slice of all active payment +// addresses in a wallet. +func (w *Wallet) SortedActivePaymentAddresses() []string { + infos := w.KeyStore.SortedActiveAddresses() + + addrs := make([]string, len(infos)) + for i, info := range infos { + addrs[i] = info.Address().EncodeAddress() + } + + return addrs +} + +// NewAddress returns the next chained address for a wallet. +func (w *Wallet) NewAddress() (btcutil.Address, error) { + // Get current block's height and hash. + bs, err := w.SyncedChainTip() + if err != nil { + return nil, err + } + + // Get next address from wallet. + addr, err := w.KeyStore.NextChainedAddress(bs) + if err != nil { + return nil, err + } + + // Immediately write updated wallet to disk. + w.KeyStore.MarkDirty() + if err := w.KeyStore.WriteIfDirty(); err != nil { + return nil, fmt.Errorf("key write failed: %v", err) + } + + // Request updates from btcd for new transactions sent to this address. + if err := w.chainSvr.NotifyReceived([]btcutil.Address{addr}); err != nil { + return nil, err + } + + return addr, nil +} + +// NewChangeAddress returns a new change address for a wallet. +func (w *Wallet) NewChangeAddress() (btcutil.Address, error) { + // Get current block's height and hash. + bs, err := w.SyncedChainTip() + if err != nil { + return nil, err + } + + // Get next chained change address from wallet. + addr, err := w.KeyStore.ChangeAddress(bs) + if err != nil { + return nil, err + } + + // Immediately write updated wallet to disk. + w.KeyStore.MarkDirty() + if err := w.KeyStore.WriteIfDirty(); err != nil { + return nil, fmt.Errorf("key write failed: %v", err) + } + + // Request updates from btcd for new transactions sent to this address. + if err := w.chainSvr.NotifyReceived([]btcutil.Address{addr}); err != nil { + return nil, err + } + + return addr, nil +} + +// RecoverAddresses recovers the next n chained addresses of a wallet. +func (w *Wallet) RecoverAddresses(n int) error { + // Get info on the last chained address. The rescan starts at the + // earliest block height the last chained address might appear at. + last := w.KeyStore.LastChainedAddress() + lastInfo, err := w.KeyStore.Address(last) + if err != nil { + return err + } + + addrs, err := w.KeyStore.ExtendActiveAddresses(n) + if err != nil { + return err + } + + // Determine the block necesary to start the rescan. + height := lastInfo.FirstBlock() + // TODO: fix our "synced to block" handling (either in + // keystore or txstore, or elsewhere) so this *always* + // returns the block hash. Looking it up by height is + // asking for problems. + hash, err := w.chainSvr.GetBlockHash(int64(height)) + if err != nil { + return err + } + + // Run a goroutine to rescan blockchain for recovered addresses. + job := &RescanJob{ + Addrs: addrs, + OutPoints: nil, + BlockStamp: keystore.BlockStamp{ + Hash: hash, + Height: height, + }, + } + // Begin rescan and do not wait for it to finish. Because the success + // or failure of the rescan is logged elsewhere and the returned channel + // does not need to be read, ignore the return value. + _ = w.SubmitRescan(job) + + return nil +} + +// ReqSpentUtxoNtfns sends a message to btcd to request updates for when +// a stored UTXO has been spent. +func (w *Wallet) ReqSpentUtxoNtfns(credits []txstore.Credit) { + ops := make([]*btcwire.OutPoint, len(credits)) + for i, c := range credits { + op := c.OutPoint() + log.Debugf("Requesting spent UTXO notifications for Outpoint "+ + "hash %s index %d", op.Hash, op.Index) + ops[i] = op + } + + if err := w.chainSvr.NotifySpent(ops); err != nil { + log.Errorf("Cannot request notifications for spent outputs: %v", + err) + } +} + +// TotalReceived iterates through a wallet's transaction history, returning the +// total amount of bitcoins received for any wallet address. Amounts received +// through multisig transactions are ignored. +func (w *Wallet) TotalReceived(confirms int) (btcutil.Amount, error) { + bs, err := w.SyncedChainTip() + if err != nil { + return 0, err + } + + var amount btcutil.Amount + for _, r := range w.TxStore.Records() { + for _, c := range r.Credits() { + // Ignore change. + if c.Change() { + continue + } + + // Tally if the appropiate number of block confirmations have passed. + if c.Confirmed(confirms, bs.Height) { + amount += c.Amount() + } + } + } + return amount, nil +} + +// TxRecord iterates through all transaction records saved in the store, +// returning the first with an equivalent transaction hash. +func (w *Wallet) TxRecord(txSha *btcwire.ShaHash) (r *txstore.TxRecord, ok bool) { + for _, r = range w.TxStore.Records() { + if *r.Tx().Sha() == *txSha { + return r, true + } + } + return nil, false +}