wallet/wallet: use new syncToBirthday and recovery methods

In this commit, we refactor the wallet's syncing logic with
syncWithChain to use the newer, simpler methods: syncToBirthday and
recovery. Along the way, we also fix a bug within the wallet where it
was possible to sync past the birthday, but not sync to tip completely
and restart, which would lead to us starting a rescan from the latest
synced height, rather than from the birthday stamp.

This commit slightly changes the wallet's syncing behavior to the
following:

  1. Ensure the wallet is synced to its birthday.
  2. Perform a recovery if requested.
  3. Check for chain reorgs.
  4. Dispatch a rescan from the current synced height.

Co-authored-by: Roei Erez <roeierez@gmail.com>
This commit is contained in:
Wilmer Paulino 2019-01-07 18:43:19 -08:00
parent 29e1f0c4fb
commit db837f1ba3
No known key found for this signature in database
GPG key ID: 6DF57B9F9514972F

View file

@ -327,274 +327,27 @@ func (w *Wallet) syncWithChain(birthdayStamp *waddrmgr.BlockStamp) error {
return err
}
// Request notifications for transactions sending to all wallet
// addresses.
var (
addrs []btcutil.Address
unspent []wtxmgr.Credit
)
err = walletdb.View(w.db, func(dbtx walletdb.ReadTx) error {
// To start, if we've yet to find our birthday stamp, we'll do so now.
if birthdayStamp == nil {
var err error
addrs, unspent, err = w.activeData(dbtx)
return err
})
if err != nil {
return err
birthdayStamp, err = w.syncToBirthday()
if err != nil {
return err
}
}
startHeight := w.Manager.SyncedTo().Height
// We'll mark this as our first sync if we don't have any unspent
// outputs as known by the wallet. This'll allow us to skip a full
// rescan at this height, and instead wait for the backend to catch up.
isInitialSync := len(unspent) == 0
isRecovery := w.recoveryWindow > 0
birthday := w.Manager.Birthday()
// TODO(jrick): How should this handle a synced height earlier than
// the chain server best block?
// When no addresses have been generated for the wallet, the rescan can
// be skipped.
//
// TODO: This is only correct because activeData above returns all
// addresses ever created, including those that don't need to be watched
// anymore. This code should be updated when this assumption is no
// longer true, but worst case would result in an unnecessary rescan.
if isInitialSync || isRecovery {
// Find the latest checkpoint's height. This lets us catch up to
// at least that checkpoint, since we're synchronizing from
// scratch, and lets us avoid a bunch of costly DB transactions
// in the case when we're using BDB for the walletdb backend and
// Neutrino for the chain.Interface backend, and the chain
// backend starts synchronizing at the same time as the wallet.
_, bestHeight, err := chainClient.GetBestBlock()
if err != nil {
return err
// If the wallet requested an on-chain recovery of its funds, we'll do
// so now.
if w.recoveryWindow > 0 {
// We'll start the recovery from our birthday unless we were
// in the middle of a previous recovery attempt. If that's the
// case, we'll resume from that point.
startHeight := birthdayStamp.Height
walletHeight := w.Manager.SyncedTo().Height
if walletHeight > startHeight {
startHeight = walletHeight
}
checkHeight := bestHeight
if len(w.chainParams.Checkpoints) > 0 {
checkHeight = w.chainParams.Checkpoints[len(
w.chainParams.Checkpoints)-1].Height
}
logHeight := checkHeight
if bestHeight > logHeight {
logHeight = bestHeight
}
log.Infof("Catching up block hashes to height %d, this will "+
"take a while...", logHeight)
// Initialize the first database transaction.
tx, err := w.db.BeginReadWriteTx()
if err != nil {
return err
}
ns := tx.ReadWriteBucket(waddrmgrNamespaceKey)
// Only allocate the recoveryMgr if we are actually in recovery
// mode.
var recoveryMgr *RecoveryManager
if isRecovery {
log.Infof("RECOVERY MODE ENABLED -- rescanning for "+
"used addresses with recovery_window=%d",
w.recoveryWindow)
// Initialize the recovery manager with a default batch
// size of 2000.
recoveryMgr = NewRecoveryManager(
w.recoveryWindow, recoveryBatchSize,
w.chainParams,
)
// In the event that this recovery is being resumed, we
// will need to repopulate all found addresses from the
// database. For basic recovery, we will only do so for
// the default scopes.
scopedMgrs, err := w.defaultScopeManagers()
if err != nil {
return err
}
txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey)
credits, err := w.TxStore.UnspentOutputs(txmgrNs)
if err != nil {
return err
}
err = recoveryMgr.Resurrect(ns, scopedMgrs, credits)
if err != nil {
return err
}
}
for height := startHeight; height <= bestHeight; height++ {
hash, err := chainClient.GetBlockHash(int64(height))
if err != nil {
tx.Rollback()
return err
}
// If we're using the Neutrino backend, we can check if
// it's current or not. For other backends we'll assume
// it is current if the best height has reached the
// last checkpoint.
isCurrent := func(bestHeight int32) bool {
switch c := chainClient.(type) {
case *chain.NeutrinoClient:
return c.CS.IsCurrent()
}
return bestHeight >= checkHeight
}
// If we've found the best height the backend knows
// about, and the backend is still synchronizing, we'll
// wait. We can give it a little bit of time to
// synchronize further before updating the best height
// based on the backend. Once we see that the backend
// has advanced, we can catch up to it.
for height == bestHeight && !isCurrent(bestHeight) {
time.Sleep(100 * time.Millisecond)
_, bestHeight, err = chainClient.GetBestBlock()
if err != nil {
tx.Rollback()
return err
}
}
header, err := chainClient.GetBlockHeader(hash)
if err != nil {
return err
}
// Check to see if this header's timestamp has surpassed
// our birthday or if we've surpassed one previously.
timestamp := header.Timestamp
if timestamp.After(birthday) || birthdayStamp != nil {
// If this is the first block past our birthday,
// record the block stamp so that we can use
// this as the starting point for the rescan.
// This will ensure we don't miss transactions
// that are sent to the wallet during an initial
// sync.
//
// NOTE: The birthday persisted by the wallet is
// two days before the actual wallet birthday,
// to deal with potentially inaccurate header
// timestamps.
if birthdayStamp == nil {
birthdayStamp = &waddrmgr.BlockStamp{
Height: height,
Hash: *hash,
Timestamp: timestamp,
}
log.Debugf("Found birthday block: "+
"height=%d, hash=%v",
birthdayStamp.Height,
birthdayStamp.Hash)
err := w.Manager.SetBirthdayBlock(
ns, *birthdayStamp, true,
)
if err != nil {
tx.Rollback()
return err
}
}
// If we are in recovery mode and the check
// passes, we will add this block to our list of
// blocks to scan for recovered addresses.
if isRecovery {
recoveryMgr.AddToBlockBatch(
hash, height, timestamp,
)
}
}
err = w.Manager.SetSyncedTo(ns, &waddrmgr.BlockStamp{
Hash: *hash,
Height: height,
Timestamp: timestamp,
})
if err != nil {
tx.Rollback()
return err
}
// If we are in recovery mode, attempt a recovery on
// blocks that have been added to the recovery manager's
// block batch thus far. If block batch is empty, this
// will be a NOP.
if isRecovery && height%recoveryBatchSize == 0 {
err := w.recoverDefaultScopes(
chainClient, tx, ns,
recoveryMgr.BlockBatch(),
recoveryMgr.State(),
)
if err != nil {
tx.Rollback()
return err
}
// Clear the batch of all processed blocks.
recoveryMgr.ResetBlockBatch()
}
// Every 10K blocks, commit and start a new database TX.
if height%10000 == 0 {
err = tx.Commit()
if err != nil {
tx.Rollback()
return err
}
log.Infof("Caught up to height %d", height)
tx, err = w.db.BeginReadWriteTx()
if err != nil {
return err
}
ns = tx.ReadWriteBucket(waddrmgrNamespaceKey)
}
}
// Perform one last recovery attempt for all blocks that were
// not batched at the default granularity of 2000 blocks.
if isRecovery {
err := w.recoverDefaultScopes(
chainClient, tx, ns, recoveryMgr.BlockBatch(),
recoveryMgr.State(),
)
if err != nil {
tx.Rollback()
return err
}
}
// Commit (or roll back) the final database transaction.
err = tx.Commit()
if err != nil {
tx.Rollback()
return err
}
log.Info("Done catching up block hashes")
// Since we've spent some time catching up block hashes, we
// might have new addresses waiting for us that were requested
// during initial sync. Make sure we have those before we
// request a rescan later on.
err = walletdb.View(w.db, func(dbtx walletdb.ReadTx) error {
var err error
addrs, unspent, err = w.activeData(dbtx)
return err
})
if err != nil {
if err := w.recovery(startHeight); err != nil {
return err
}
}
@ -685,15 +438,21 @@ func (w *Wallet) syncWithChain(birthdayStamp *waddrmgr.BlockStamp) error {
return err
}
// If this was our initial sync, we're recovering from our seed, or our
// birthday was rolled back due to a chain reorg, we'll dispatch a
// rescan from our birthday block to ensure we detect all relevant
// on-chain events from this point.
if isInitialSync || isRecovery || birthdayRollback {
return w.rescanWithTarget(addrs, unspent, birthdayStamp)
// Finally, we'll trigger a wallet rescan from the currently synced tip
// and request notifications for transactions sending to all wallet
// addresses and spending all wallet UTXOs.
var (
addrs []btcutil.Address
unspent []wtxmgr.Credit
)
err = walletdb.View(w.db, func(dbtx walletdb.ReadTx) error {
addrs, unspent, err = w.activeData(dbtx)
return err
})
if err != nil {
return err
}
// Otherwise, we'll rescan from tip.
return w.rescanWithTarget(addrs, unspent, nil)
}