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:
parent
29e1f0c4fb
commit
db837f1ba3
1 changed files with 30 additions and 271 deletions
301
wallet/wallet.go
301
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)
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue