Sync wallet, utxo, and tx files to disk.

This runs a syncer once every minute to write any dirty wallet data
structures out to disk.  As currently implemented, dirty wallets will
be lost if not written before btcwallet closes or crashes.
Deterministic wallet help migitate this issue (as private keys can be
created again as long as a previous wallet file was written) but this
can still be a nuisance as a longer rescan will be required to catch
up to chain.
This commit is contained in:
Josh Rickmar 2013-10-15 15:53:49 -04:00
parent 9b84f87930
commit 284191ec4b
3 changed files with 150 additions and 1 deletions

13
cmd.go
View file

@ -107,10 +107,16 @@ func (s *BtcWalletStore) Rollback(height int64, hash *btcwire.ShaHash) {
func (w *BtcWallet) Rollback(height int64, hash *btcwire.ShaHash) {
w.UtxoStore.Lock()
w.UtxoStore.dirty = w.UtxoStore.dirty || w.UtxoStore.s.Rollback(height, hash)
if w.UtxoStore.dirty {
AddDirtyAccount(w)
}
w.UtxoStore.Unlock()
w.TxStore.Lock()
w.TxStore.dirty = w.TxStore.dirty || w.TxStore.s.Rollback(height, hash)
if w.TxStore.dirty {
AddDirtyAccount(w)
}
w.TxStore.Unlock()
}
@ -449,6 +455,7 @@ func (w *BtcWallet) newBlockTxHandler(result interface{}, e *btcjson.Error) bool
txs := w.TxStore.s
w.TxStore.s = append(txs, t)
w.TxStore.dirty = true
AddDirtyAccount(w)
w.TxStore.Unlock()
}()
@ -469,6 +476,7 @@ func (w *BtcWallet) newBlockTxHandler(result interface{}, e *btcjson.Error) bool
w.UtxoStore.Lock()
w.UtxoStore.s = append(w.UtxoStore.s, u)
w.UtxoStore.dirty = true
AddDirtyAccount(w)
w.UtxoStore.Unlock()
confirmed := w.CalculateBalance(6)
unconfirmed := w.CalculateBalance(0) - confirmed
@ -505,7 +513,7 @@ func main() {
}
cfg = tcfg
// Open wallet
// Open default wallet
w, err := OpenWallet(cfg, "")
if err != nil {
log.Info(err.Error())
@ -530,6 +538,9 @@ func main() {
// Begin generating new IDs for JSON calls.
go JSONIDGenerator(NewJSONID)
// Begin wallet to disk syncer.
go DirtyAccountUpdater()
for {
replies := make(chan error)
done := make(chan int)

View file

@ -493,6 +493,7 @@ func SendFrom(reply chan []byte, msg *btcjson.Message) {
modified := w.UtxoStore.s.Remove(inputs)
if modified {
w.UtxoStore.dirty = true
AddDirtyAccount(w)
w.UtxoStore.Unlock()
// Notify all frontends of new account balances.
@ -637,6 +638,7 @@ func SendMany(reply chan []byte, msg *btcjson.Message) {
modified := w.UtxoStore.s.Remove(inputs)
if modified {
w.UtxoStore.dirty = true
AddDirtyAccount(w)
w.UtxoStore.Unlock()
// Notify all frontends of new account balances.
@ -768,6 +770,7 @@ func CreateEncryptedWallet(reply chan []byte, msg *btcjson.Message) {
bw := &BtcWallet{
Wallet: w,
name: wname,
dirty: true,
NewBlockTxSeqN: n,
}
// TODO(jrick): only begin tracking wallet if btcwallet is already
@ -775,6 +778,7 @@ func CreateEncryptedWallet(reply chan []byte, msg *btcjson.Message) {
bw.Track()
wallets.m[wname] = bw
AddDirtyAccount(bw)
ReplySuccess(reply, msg.Id, nil)
}

134
disksync.go Normal file
View file

@ -0,0 +1,134 @@
/*
* Copyright (c) 2013 Conformal Systems LLC <info@conformal.com>
*
* 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 (
"fmt"
"os"
"path/filepath"
"time"
)
var dirtyAccountSet = make(map[*BtcWallet]bool)
var addDirtyAccount = make(chan *BtcWallet)
// DirtyAccountUpdater is responsible for listening for listens for new
// dirty wallets (changed in memory with updaets not yet saved to disk)
// to add to dirtyAccountSet. This is designed to run as a single goroutine.
func DirtyAccountUpdater() {
timer := time.Tick(time.Minute)
for {
select {
case w := <-addDirtyAccount:
dirtyAccountSet[w] = true
case <-timer:
for w := range dirtyAccountSet {
if err := w.writeDirtyToDisk(); err != nil {
log.Errorf("cannot sync dirty wallet '%v': %v", w.name, err)
} else {
delete(dirtyAccountSet, w)
log.Infof("removed dirty wallet '%v'", w.name)
}
}
}
}
}
// AddDirtyAccount adds w to a set of items to be synced to disk. The
// dirty flag must still be set on the various dirty elements of the
// account (wallet, transactions, and/or utxos) or nothing will be
// written to disk during the next scheduled sync.
func AddDirtyAccount(w *BtcWallet) {
addDirtyAccount <- w
}
// writeDirtyToDisk checks for the dirty flag on an account's wallet,
// txstore, and utxostore, writing them to disk if any are dirty.
func (w *BtcWallet) writeDirtyToDisk() error {
// Temporary files append the current time to the normal file name.
// In caes of failure, the most recent temporary file can be inspected
// for validity, and moved to replace the main file.
timeStr := fmt.Sprintf("%v", time.Now().Unix())
wdir := walletdir(cfg, w.name)
wfilepath := filepath.Join(wdir, "wallet.bin")
txfilepath := filepath.Join(wdir, "tx.bin")
utxofilepath := filepath.Join(wdir, "utxo.bin")
// Wallet
if w.dirty {
w.mtx.RLock()
defer w.mtx.RUnlock()
tmpfilepath := wfilepath + "-" + timeStr
tmpfile, err := os.Create(tmpfilepath)
if err != nil {
return err
}
if _, err = w.WriteTo(tmpfile); err != nil {
return err
}
// TODO(jrick): this should be atomic on *nix, but is not on
// Windows. Use _windows.go to provide atomic renames.
if err = os.Rename(tmpfilepath, wfilepath); err != nil {
return err
}
}
// Transactions
if w.TxStore.dirty {
w.TxStore.RLock()
defer w.TxStore.RUnlock()
tmpfilepath := txfilepath + "-" + timeStr
tmpfile, err := os.Create(tmpfilepath)
if err != nil {
return err
}
if _, err = w.TxStore.s.WriteTo(tmpfile); err != nil {
return err
}
// TODO(jrick): this should be atomic on *nix, but is not on
// Windows. Use _windows.go to provide atomic renames.
if err = os.Rename(tmpfilepath, txfilepath); err != nil {
return err
}
}
// UTXOs
if w.UtxoStore.dirty {
w.UtxoStore.RLock()
defer w.UtxoStore.RUnlock()
tmpfilepath := utxofilepath + "-" + timeStr
tmpfile, err := os.Create(tmpfilepath)
if err != nil {
return err
}
if _, err = w.UtxoStore.s.WriteTo(tmpfile); err != nil {
return err
}
// TODO(jrick): this should be atomic on *nix, but is not on
// Windows. Use _windows.go to provide atomic renames.
if err = os.Rename(tmpfilepath, utxofilepath); err != nil {
return err
}
}
return nil
}