diff --git a/account.go b/account.go index 2d31eac..3a43d72 100644 --- a/account.go +++ b/account.go @@ -70,53 +70,17 @@ type Account struct { *wallet.Wallet mtx sync.RWMutex name string - dirty bool fullRescan bool UtxoStore struct { sync.RWMutex - dirty bool - s tx.UtxoStore + s tx.UtxoStore } TxStore struct { sync.RWMutex - dirty bool - s tx.TxStore + s tx.TxStore } } -// MarkDirtyWallet marks an account's wallet as dirty, and adds the -// account to the list of dirty accounts to be schedule to be synced to -// disk. It is a runtime error to call this without holding the wallet -// writer lock. -func (a *Account) MarkDirtyWallet() { - a.dirty = true - dirtyAccounts.Lock() - dirtyAccounts.m[a] = true - dirtyAccounts.Unlock() -} - -// MarkDirtyUtxoStore marks an account's utxo store as dirty, and adds -// the account to the list of dirty accounts to be schedule to be synced to -// disk. It is a runtime error to call this without holding the utxo store -// writer lock. -func (a *Account) MarkDirtyUtxoStore() { - a.UtxoStore.dirty = true - dirtyAccounts.Lock() - dirtyAccounts.m[a] = true - dirtyAccounts.Unlock() -} - -// MarkDirtyTxStore marks an account's tx store as dirty, and adds the -// account to the list of dirty accounts to be schedule to be synced to -// disk. It is a runtime error to call this without holding the tx store -// writer lock. -func (a *Account) MarkDirtyTxStore() { - a.TxStore.dirty = true - dirtyAccounts.Lock() - dirtyAccounts.m[a] = true - dirtyAccounts.Unlock() -} - // Lock locks the underlying wallet for an account. func (a *Account) Lock() error { a.mtx.Lock() @@ -155,15 +119,17 @@ func (a *Account) Unlock(passphrase []byte) error { // that occured on a chain no longer considered to be the main chain. func (a *Account) Rollback(height int32, hash *btcwire.ShaHash) { a.UtxoStore.Lock() - a.UtxoStore.dirty = a.UtxoStore.dirty || a.UtxoStore.s.Rollback(height, hash) + modified := a.UtxoStore.s.Rollback(height, hash) a.UtxoStore.Unlock() + if modified { + a.ScheduleUtxoStoreWrite() + } a.TxStore.Lock() - a.TxStore.dirty = a.TxStore.dirty || a.TxStore.s.Rollback(height, hash) + modified = a.TxStore.s.Rollback(height, hash) a.TxStore.Unlock() - - if err := a.writeDirtyToDisk(); err != nil { - log.Errorf("cannot sync dirty wallet: %v", err) + if modified { + a.ScheduleTxStoreWrite() } } @@ -442,7 +408,7 @@ func (a *Account) ImportPrivKey(wif string, rescan bool) error { } Rescan(CurrentRPCConn(), bs.Height, addrs) - a.writeDirtyToDisk() + a.WriteScheduledToDisk() }() } return nil @@ -464,22 +430,16 @@ func (a *Account) ImportWIFPrivateKey(wif string, bs *wallet.BlockStamp) (string // Attempt to import private key into wallet. a.mtx.Lock() addr, err := a.Wallet.ImportPrivateKey(privkey, compressed, bs) + a.mtx.Unlock() if err != nil { - a.mtx.Unlock() return "", err } addrStr := addr.String() - // Immediately write dirty wallet to disk. - // - // TODO(jrick): change writeDirtyToDisk to not grab the writer lock. - // Don't want to let another goroutine waiting on the mutex to grab - // the mutex before it is written to disk. - a.dirty = true - a.mtx.Unlock() - if err := a.writeDirtyToDisk(); err != nil { - log.Errorf("cannot write dirty wallet: %v", err) - return "", fmt.Errorf("import failed: cannot write wallet: %v", err) + // Immediately write wallet to disk. + a.ScheduleWalletWrite() + if err := a.WriteScheduledToDisk(); err != nil { + return "", fmt.Errorf("cannot write account: %v", err) } // Associate the imported address with this account. @@ -600,7 +560,7 @@ func (a *Account) RescanActiveAddresses() { // Rescan active addresses starting at the determined block height. Rescan(CurrentRPCConn(), beginBlock, a.ActivePaymentAddresses()) - a.writeDirtyToDisk() + a.WriteScheduledToDisk() } // SortedActivePaymentAddresses returns a slice of all active payment @@ -641,20 +601,18 @@ func (a *Account) NewAddress() (btcutil.Address, error) { return nil, err } - a.mtx.Lock() - // Get next address from wallet. + a.mtx.Lock() addr, err := a.Wallet.NextChainedAddress(&bs, cfg.KeypoolSize) + a.mtx.Unlock() if err != nil { - a.mtx.Unlock() return nil, err } // Immediately write updated wallet to disk. - a.dirty = true - a.mtx.Unlock() - if err = a.writeDirtyToDisk(); err != nil { - log.Errorf("cannot sync dirty wallet: %v", err) + a.ScheduleWalletWrite() + if err := a.WriteScheduledToDisk(); err != nil { + return nil, fmt.Errorf("account write failed: %v", err) } // Mark this new address as belonging to this account. diff --git a/accountstore.go b/accountstore.go index 458929b..bcd9099 100644 --- a/accountstore.go +++ b/accountstore.go @@ -86,34 +86,24 @@ func (store *AccountStore) BlockNotify(bs *wallet.BlockStamp) { store.RLock() defer store.RUnlock() - for _, a := range store.accounts { - // The UTXO store will be dirty if it was modified - // from a tx notification. - if a.UtxoStore.dirty { - // Notify all frontends of account's new unconfirmed - // and confirmed balance. - confirmed := a.CalculateBalance(1) - unconfirmed := a.CalculateBalance(0) - confirmed - NotifyWalletBalance(frontendNotificationMaster, - a.name, confirmed) - NotifyWalletBalanceUnconfirmed(frontendNotificationMaster, - a.name, unconfirmed) - } + for name, a := range store.accounts { + // 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 := a.CalculateBalance(1) + unconfirmed := a.CalculateBalance(0) - confirmed + NotifyWalletBalance(frontendNotificationMaster, a.name, confirmed) + NotifyWalletBalanceUnconfirmed(frontendNotificationMaster, a.name, + unconfirmed) - // The account is intentionaly not immediately synced to disk. - // If btcd is performing an IBD, writing the wallet file for - // each newly-connected block would result in too many - // unnecessary disk writes. The UTXO and transaction stores - // could be written, but in the case of btcwallet closing - // before writing the dirty wallet, both would have to be - // pruned anyways. - // - // Instead, the wallet is queued to be written to disk at the - // next scheduled disk sync. - a.mtx.Lock() - a.Wallet.SetSyncedWith(bs) - a.MarkDirtyWallet() - a.mtx.Unlock() + // If this is the default account, update the block all accounts + // are synced with, and schedule a wallet write. + if name == "" { + a.mtx.Lock() + a.Wallet.SetSyncedWith(bs) + a.mtx.Unlock() + a.ScheduleWalletWrite() + } } } @@ -121,7 +111,7 @@ func (store *AccountStore) BlockNotify(bs *wallet.BlockStamp) { // 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 -// marked as dirty. +// scheduled to be written to disk.. func (store *AccountStore) RecordMinedTx(txid *btcwire.ShaHash, blkhash *btcwire.ShaHash, blkheight int32, blkindex int, blktime int64) error { @@ -141,19 +131,17 @@ func (store *AccountStore) RecordMinedTx(txid *btcwire.ShaHash, sendtx, ok := account.TxStore.s[i].(*tx.SendTx) if ok { if bytes.Equal(txid.Bytes(), sendtx.TxID[:]) { - // Unlock the held reader lock and wait for - // the writer lock. account.TxStore.RUnlock() - account.TxStore.Lock() + account.TxStore.Lock() copy(sendtx.BlockHash[:], blkhash.Bytes()) sendtx.BlockHeight = blkheight sendtx.BlockIndex = int32(blkindex) sendtx.BlockTime = blktime - account.MarkDirtyTxStore() - - // Release writer lock and return. account.TxStore.Unlock() + + account.ScheduleTxStoreWrite() + return nil } } @@ -205,10 +193,10 @@ func (store *AccountStore) CreateEncryptedWallet(name, desc string, passphrase [ account := &Account{ Wallet: wlt, name: name, - dirty: true, } - account.UtxoStore.dirty = true - account.TxStore.dirty = true + account.ScheduleWalletWrite() + account.ScheduleTxStoreWrite() + account.ScheduleUtxoStoreWrite() // Mark all active payment addresses as belonging to this account. for addr := range account.ActivePaymentAddresses() { @@ -227,8 +215,8 @@ func (store *AccountStore) CreateEncryptedWallet(name, desc string, passphrase [ // TODO(jrick): this should *only* happen if btcd is connected. account.Track() - // Write new wallet to disk. - if err := account.writeDirtyToDisk(); err != nil { + // Ensure that the account is written out to disk. + if err := account.WriteScheduledToDisk(); err != nil { return err } @@ -237,15 +225,30 @@ func (store *AccountStore) CreateEncryptedWallet(name, desc string, passphrase [ // ChangePassphrase unlocks all account wallets with the old // passphrase, and re-encrypts each using the new passphrase. +// +// TODO(jrick): this is a perfect example of how awful the account +// locking is. It must be replaced. func (store *AccountStore) ChangePassphrase(old, new []byte) error { - store.RLock() - defer store.RUnlock() + // Due to the undefined order of ranging over the accountstore + // map and how all account wallet writer locks are grabbed + // simultaneously and unlocked with a defer, this function is + // unsafe to call simulateously with other accountstore functions, + // even though the store itself is not modified. + store.Lock() + defer store.Unlock() + if err := store.changePassphrase(old, new); err != nil { + return err + } + + // Immediately write out to disk. + return store.WriteAllToDisk() +} + +// changePassphrase changes all passphrases for all accounts without grabbing +// any accountstore locks. +func (store *AccountStore) changePassphrase(old, new []byte) error { // Check that each account can be unlocked with the old passphrase. - // Each's account's wallet mutex is unlocked with a defer so they - // will be held for the duration of this function. This prevents - // a wallet from being locked after some timeout after a RPC call - // to walletpassphrase. for _, a := range store.accounts { a.mtx.Lock() defer a.mtx.Unlock() @@ -267,38 +270,6 @@ func (store *AccountStore) ChangePassphrase(old, new []byte) error { if err := a.Wallet.ChangePassphrase(new); err != nil { return err } - a.dirty = true - } - - // Immediately write out to disk. Create a new temporary network - // directory to write to, write all account files there, then move - // to the real network directory. This provides an safe - // replacement of all account files and ensures that all wallets - // are using either the old or new passphrase, but never two wallets - // with different passphrases. - netDir := networkDir(cfg.Net()) - tmpNetDir := tmpNetworkDir(cfg.Net()) - for _, a := range store.accounts { - // Writer locks must be held for the tx and utxo stores as well, - // to unset the dirty flag. - a.UtxoStore.Lock() - defer a.UtxoStore.Unlock() - a.TxStore.Lock() - defer a.TxStore.Unlock() - - if err := a.writeAllToFreshDir(tmpNetDir); 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(tmpNetDir, netDir); err != nil { - return err } return nil diff --git a/btcdrpc.go b/btcdrpc.go index 9437df2..22bb666 100644 --- a/btcdrpc.go +++ b/btcdrpc.go @@ -368,8 +368,8 @@ func NtfnProcessedTx(n btcjson.Cmd, marshaled []byte) { // Record the tx history. a.TxStore.Lock() a.TxStore.s.InsertRecvTx(t) - a.MarkDirtyTxStore() a.TxStore.Unlock() + a.ScheduleTxStoreWrite() // Notify frontends of tx. If the tx is unconfirmed, it is always // notified and the outpoint is marked as notified. If the outpoint @@ -404,8 +404,8 @@ func NtfnProcessedTx(n btcjson.Cmd, marshaled []byte) { copy(u.BlockHash[:], blockHash[:]) a.UtxoStore.Lock() a.UtxoStore.s.Insert(u) - a.MarkDirtyUtxoStore() a.UtxoStore.Unlock() + a.ScheduleUtxoStoreWrite() // If this notification came from mempool, notify frontends of // the new unconfirmed balance immediately. Otherwise, wait until diff --git a/cmd.go b/cmd.go index 3f38543..91bb815 100644 --- a/cmd.go +++ b/cmd.go @@ -160,7 +160,7 @@ func main() { } // Start account disk syncer goroutine. - go DirtyAccountSyncer() + go AccountDiskSyncer() go func() { s, err := newServer(cfg.SvrListeners) diff --git a/cmdmgr.go b/cmdmgr.go index a725a78..4246ff8 100644 --- a/cmdmgr.go +++ b/cmdmgr.go @@ -278,7 +278,7 @@ func ExportWatchingWallet(icmd btcjson.Cmd) (interface{}, *btcjson.Error) { } // Create export directory, write files there. - if err = wa.WriteExport("watchingwallet"); err != nil { + if err = wa.ExportToDirectory("watchingwallet"); err != nil { e := btcjson.Error{ Code: btcjson.ErrWallet.Code, Message: err.Error(), @@ -942,12 +942,16 @@ func SendFrom(icmd btcjson.Cmd) (interface{}, *btcjson.Error) { // wait until all send history has been written. SendTxHistSyncChans.add <- createdTx.txid - // If a change address was added, mark wallet as dirty, sync to disk, - // and request updates for change address. + // If a change address was added, sync wallet to disk and request + // transaction notifications to the change address. if createdTx.changeAddr != nil { - a.dirty = true - if err := a.writeDirtyToDisk(); err != nil { - log.Errorf("cannot write dirty wallet: %v", err) + a.ScheduleWalletWrite() + if err := a.WriteScheduledToDisk(); err != nil { + e := btcjson.Error{ + Code: btcjson.ErrWallet.Code, + Message: "Cannot write account: " + err.Error(), + } + return nil, &e } a.ReqNewTxsForAddress(createdTx.changeAddr) } @@ -1021,12 +1025,16 @@ func SendMany(icmd btcjson.Cmd) (interface{}, *btcjson.Error) { // wait until all send history has been written. SendTxHistSyncChans.add <- createdTx.txid - // If a change address was added, mark wallet as dirty, sync to disk, - // and request updates for change address. + // If a change address was added, sync wallet to disk and request + // transaction notifications to the change address. if createdTx.changeAddr != nil { - a.dirty = true - if err := a.writeDirtyToDisk(); err != nil { - log.Errorf("cannot write dirty wallet: %v", err) + a.ScheduleWalletWrite() + if err := a.WriteScheduledToDisk(); err != nil { + e := btcjson.Error{ + Code: btcjson.ErrWallet.Code, + Message: "Cannot write account: " + err.Error(), + } + return nil, &e } a.ReqNewTxsForAddress(createdTx.changeAddr) } @@ -1122,8 +1130,8 @@ func handleSendRawTxReply(icmd btcjson.Cmd, txIDStr string, a *Account, txInfo * } a.TxStore.Lock() a.TxStore.s = append(a.TxStore.s, sendtx) - a.TxStore.dirty = true a.TxStore.Unlock() + a.ScheduleTxStoreWrite() // Notify frontends of new SendTx. bs, err := GetCurBlock() @@ -1140,12 +1148,14 @@ func handleSendRawTxReply(icmd btcjson.Cmd, txIDStr string, a *Account, txInfo * // Remove previous unspent outputs now spent by the tx. a.UtxoStore.Lock() modified := a.UtxoStore.s.Remove(txInfo.inputs) - a.UtxoStore.dirty = a.UtxoStore.dirty || modified a.UtxoStore.Unlock() + if modified { + a.ScheduleUtxoStoreWrite() + } // Disk sync tx and utxo stores. - if err := a.writeDirtyToDisk(); err != nil { - log.Errorf("cannot sync dirty wallet: %v", err) + if err := a.WriteScheduledToDisk(); err != nil { + log.Errorf("cannot write account: %v", err) } // Notify all frontends of account's new unconfirmed and diff --git a/disksync.go b/disksync.go index 202ed03..206f6dc 100644 --- a/disksync.go +++ b/disksync.go @@ -22,20 +22,9 @@ import ( "io/ioutil" "os" "path/filepath" - "sync" "time" ) -var ( - // dirtyAccounts holds a set of accounts that include dirty components. - dirtyAccounts = struct { - sync.Mutex - m map[*Account]bool - }{ - m: make(map[*Account]bool), - } -) - // networkDir returns the directory name of a network directory to hold account // files. func networkDir(net btcwire.BitcoinNet) string { @@ -53,6 +42,33 @@ func tmpNetworkDir(net btcwire.BitcoinNet) 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 fd.Close() + 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 { @@ -87,259 +103,320 @@ func accountFilename(suffix, account, netdir string) string { return filepath.Join(netdir, fmt.Sprintf("%v-%v", account, suffix)) } -// DirtyAccountSyncer synces dirty accounts for cases where the updated -// information was not required to be immediately written to disk. Accounts -// may be added to dirtyAccounts and will be checked and processed every 10 -// seconds by this function. +// 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{} + utxos map[*Account]struct{} +} + +func newSyncSchedule(dir string) *syncSchedule { + s := &syncSchedule{ + dir: dir, + wallets: make(map[*Account]struct{}), + txs: make(map[*Account]struct{}), + utxos: 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.utxos[a]; ok { + if err := a.writeUtxoStore(s.dir); err != nil { + return err + } + delete(s.utxos, a) + } + 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.utxos { + if err := a.writeUtxoStore(s.dir); err != nil { + return err + } + delete(s.utxos, a) + } + + 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 +} + +// Channels for AccountDiskSyncer. +var ( + scheduleWalletWrite = make(chan *Account) + scheduleTxStoreWrite = make(chan *Account) + scheduleUtxoStoreWrite = make(chan *Account) + syncBatch = make(chan *syncBatchRequest) + syncAccount = make(chan *syncRequest) + exportAccount = make(chan *exportRequest) +) + +type syncRequest struct { + a *Account + err chan error +} + +type syncBatchRequest struct { + a []*Account + err chan error +} + +type exportRequest struct { + dir string + a *Account + err chan error +} + +// AccountDiskSyncer manages a set of "dirty" account files which must +// be written to disk, and synchronizes all writes in a single goroutine. +// After 10 seconds since the latest sync, all unwritten files are written +// and removed. Writes for a single account may be scheduled immediately by +// calling WriteScheduledToDisk. // // This never returns and is meant to be called from a goroutine. -func DirtyAccountSyncer() { +func AccountDiskSyncer() { + netdir := networkDir(cfg.Net()) + if err := checkCreateDir(netdir); err != nil { + log.Errorf("Unable to create or write to account directory: %v", err) + } + tmpnetdir := tmpNetworkDir(cfg.Net()) + + schedule := newSyncSchedule(netdir) ticker := time.Tick(10 * time.Second) for { select { - case <-ticker: - dirtyAccounts.Lock() - for a := range dirtyAccounts.m { - log.Debugf("Syncing account '%v' to disk", - a.Wallet.Name()) - if err := a.writeDirtyToDisk(); err != nil { - log.Errorf("cannot sync dirty wallet: %v", - err) - } else { - delete(dirtyAccounts.m, a) - } + case a := <-scheduleWalletWrite: + schedule.wallets[a] = struct{}{} + + case a := <-scheduleTxStoreWrite: + schedule.txs[a] = struct{}{} + + case a := <-scheduleUtxoStoreWrite: + schedule.utxos[a] = struct{}{} + + case sr := <-syncAccount: + sr.err <- schedule.FlushAccount(sr.a) + + case sr := <-syncBatch: + err := batchWriteAccounts(sr.a, tmpnetdir, netdir) + if err == nil { + // All accounts have been synced, old schedule + // can be discarded. + schedule = newSyncSchedule(netdir) + } + sr.err <- err + + case er := <-exportAccount: + a := er.a + dir := er.dir + er.err <- a.writeAll(dir) + + case <-ticker: + if err := schedule.Flush(); err != nil { + log.Errorf("Cannot write account: %v", err) } - dirtyAccounts.Unlock() } } } -// 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 fd.Close() - 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 -} - -// writeAllToFreshDir writes all account files to the specified directory. -// If dir already exists, any old files are removed. If dir does not -// exist, it is created. +// WriteAllToDisk writes all account files for all accounts at once. Unlike +// writing individual account files, this causes each account file to be +// written to a new network directory to replace the old one. Use this +// function when it is needed to ensure an all or nothing write for all +// account files. // -// It is a runtime error to call this function while not holding each -// wallet, tx store, and utxo store writer lock. -func (a *Account) writeAllToFreshDir(dir string) error { - if err := freshDir(dir); err != nil { - return err +// It is a runtime error to call this without holding the store writer lock. +func (store *AccountStore) WriteAllToDisk() error { + accts := make([]*Account, 0, len(store.accounts)) + for _, a := range store.accounts { + accts = append(accts, a) } + err := make(chan error, 1) + syncBatch <- &syncBatchRequest{ + a: accts, + err: err, + } + 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 +} + +// WriteScheduledToDisk signals AccountDiskSyncer to write all scheduled +// account files for a to disk now instead of waiting for the next sync +// interval. This function blocks until all the file writes for a have +// finished, and returns a non-nil error if any of the file writes failed. +func (a *Account) WriteScheduledToDisk() error { + err := make(chan error, 1) + syncAccount <- &syncRequest{ + a: a, + err: err, + } + return <-err +} + +// ScheduleWalletWrite schedules a write of an account's wallet file. +func (a *Account) ScheduleWalletWrite() { + scheduleWalletWrite <- a +} + +// ScheduleTxStoreWrite schedules a write of an account's tx store file. +func (a *Account) ScheduleTxStoreWrite() { + scheduleTxStoreWrite <- a +} + +// ScheduleUtxoStoreWrite schedules a write of an account's utxo store file. +func (a *Account) ScheduleUtxoStoreWrite() { + scheduleUtxoStoreWrite <- a +} + +// 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(cfg.Net()), dirBaseName) + if err := checkCreateDir(dir); err != nil { + return err + } + fmt.Println("exporting to %v", dir) + + err := make(chan error) + er := &exportRequest{ + dir: dir, + a: a, + err: err, + } + exportAccount <- er + return <-err +} + +func (a *Account) writeAll(dir string) error { + if err := a.writeUtxoStore(dir); err != nil { + return err + } + 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 + } + defer tmpfile.Close() + + a.mtx.RLock() + _, err = a.Wallet.WriteTo(tmpfile) + a.mtx.RUnlock() + if err != nil { + return err + } + + if err = Rename(tmpfile.Name(), wfilepath); err != nil { + return err + } + + return nil +} + +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 + } + defer tmpfile.Close() + + a.TxStore.RLock() + _, err = a.TxStore.s.WriteTo(tmpfile) + a.TxStore.RUnlock() + if err != nil { + return err + } + + if err = Rename(tmpfile.Name(), txfilepath); err != nil { + return err + } + + return nil +} + +func (a *Account) writeUtxoStore(dir string) error { utxofilepath := accountFilename("utxo.bin", a.name, dir) - - wfile, err := os.Create(wfilepath) + _, filename := filepath.Split(utxofilepath) + tmpfile, err := ioutil.TempFile(dir, filename) if err != nil { return err } - defer wfile.Close() - txfile, err := os.Create(txfilepath) - if err != nil { - return err - } - defer txfile.Close() - utxofile, err := os.Create(utxofilepath) - if err != nil { - return err - } - defer utxofile.Close() + defer tmpfile.Close() - if _, err := a.Wallet.WriteTo(wfile); err != nil { - return err - } - a.dirty = false - - if _, err := a.TxStore.s.WriteTo(txfile); err != nil { - return err - } - a.TxStore.dirty = false - - if _, err := a.UtxoStore.s.WriteTo(utxofile); err != nil { - return err - } - a.UtxoStore.dirty = false - - return nil -} - -// writeDirtyToDisk checks for the dirty flag on an account's wallet, -// txstore, and utxostore, writing them to disk if any are dirty. -func (a *Account) writeDirtyToDisk() error { - netdir := networkDir(cfg.Net()) - if err := checkCreateDir(netdir); err != nil { - return err - } - - wfilepath := accountFilename("wallet.bin", a.name, netdir) - txfilepath := accountFilename("tx.bin", a.name, netdir) - utxofilepath := accountFilename("utxo.bin", a.name, netdir) - - // UTXOs and transactions are synced to disk first. This prevents - // any races from saving a wallet marked to be synced with block N - // and btcwallet closing while the UTXO and Tx files are only synced - // with block N-1. - - // UTXOs a.UtxoStore.RLock() - dirty := a.UtxoStore.dirty - a.UtxoStore.RUnlock() - if dirty { - netdir, filename := filepath.Split(utxofilepath) - tmpfile, err := ioutil.TempFile(netdir, filename) - if err != nil { - return err - } - defer tmpfile.Close() - - a.UtxoStore.RLock() - _, err = a.UtxoStore.s.WriteTo(tmpfile) - a.UtxoStore.RUnlock() - if err != nil { - return err - } - - if err = Rename(tmpfile.Name(), utxofilepath); err != nil { - return err - } - - a.UtxoStore.Lock() - a.UtxoStore.dirty = false - a.UtxoStore.Unlock() - } - - // Transactions - a.TxStore.RLock() - dirty = a.TxStore.dirty - a.TxStore.RUnlock() - if dirty { - netdir, filename := filepath.Split(txfilepath) - tmpfile, err := ioutil.TempFile(netdir, filename) - if err != nil { - return err - } - defer tmpfile.Close() - - a.TxStore.RLock() - _, err = a.TxStore.s.WriteTo(tmpfile) - a.TxStore.RUnlock() - if err != nil { - return err - } - - if err = Rename(tmpfile.Name(), txfilepath); err != nil { - return err - } - - a.TxStore.Lock() - a.TxStore.dirty = false - a.TxStore.Unlock() - } - - // Wallet - a.mtx.RLock() - dirty = a.dirty - a.mtx.RUnlock() - if dirty { - netdir, filename := filepath.Split(wfilepath) - tmpfile, err := ioutil.TempFile(netdir, filename) - if err != nil { - return err - } - defer tmpfile.Close() - - a.mtx.RLock() - _, err = a.Wallet.WriteTo(tmpfile) - a.mtx.RUnlock() - if err != nil { - return err - } - - if err = Rename(tmpfile.Name(), wfilepath); err != nil { - return err - } - - a.mtx.Lock() - a.dirty = false - a.mtx.Unlock() - } - - return nil -} - -// WriteExport writes an account to a special export directory named -// by dirName. Any previous files are overwritten. -func (a *Account) WriteExport(dirName string) error { - exportPath := filepath.Join(networkDir(cfg.Net()), dirName) - if err := checkCreateDir(exportPath); err != nil { - return err - } - - aname := a.Name() - wfilepath := accountFilename("wallet.bin", aname, exportPath) - txfilepath := accountFilename("tx.bin", aname, exportPath) - utxofilepath := accountFilename("utxo.bin", aname, exportPath) - - utxofile, err := os.Create(utxofilepath) - if err != nil { - return err - } - defer utxofile.Close() - a.UtxoStore.RLock() - _, err = a.UtxoStore.s.WriteTo(utxofile) + _, err = a.UtxoStore.s.WriteTo(tmpfile) a.UtxoStore.RUnlock() if err != nil { return err } - txfile, err := os.Create(txfilepath) - if err != nil { - return err - } - defer txfile.Close() - a.TxStore.RLock() - _, err = a.TxStore.s.WriteTo(txfile) - a.TxStore.RUnlock() - if err != nil { - return err - } - - wfile, err := os.Create(wfilepath) - if err != nil { - return err - } - defer wfile.Close() - a.mtx.RLock() - _, err = a.Wallet.WriteTo(wfile) - a.mtx.RUnlock() - if err != nil { + if err = Rename(tmpfile.Name(), utxofilepath); err != nil { return err }