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 +}