diff --git a/wallet/wallet.go b/wallet/wallet.go index eccda56..c7c7baa 100644 --- a/wallet/wallet.go +++ b/wallet/wallet.go @@ -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) }