From 603e03de3214adbfa1b50ced785ddffb4611de42 Mon Sep 17 00:00:00 2001 From: Wilmer Paulino Date: Mon, 5 Nov 2018 17:06:55 -0800 Subject: [PATCH] wallet/chainntnfs: add sanity check for birthday block before syncing In this commit, we add a sanity check for the wallet's birthday block before syncing as a result of the migration that populated it for existing wallets. This is done as the second part to the migration to ensure we do not miss any relevant events throughout rescans. The sanity check performs two main checks: whether the birthday block timestamp reflects a time before the birthday timestamp and whether the delta between these two timestamps is a reasonable amount. The birthday block is then found as the first candidate that satisfies both of these conditions. --- wallet/chainntfns.go | 205 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 203 insertions(+), 2 deletions(-) diff --git a/wallet/chainntfns.go b/wallet/chainntfns.go index 5c37870..f6b32f0 100644 --- a/wallet/chainntfns.go +++ b/wallet/chainntfns.go @@ -6,7 +6,9 @@ package wallet import ( "bytes" + "fmt" "strings" + "time" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcwallet/chain" @@ -24,7 +26,7 @@ func (w *Wallet) handleChainNotifications() { return } - sync := func(w *Wallet) { + sync := func(w *Wallet, birthdayStamp *waddrmgr.BlockStamp) { // At the moment there is no recourse if the rescan fails for // some reason, however, the wallet will not be marked synced // and many methods will error early since the wallet is known @@ -96,7 +98,20 @@ func (w *Wallet) handleChainNotifications() { var err error switch n := n.(type) { case chain.ClientConnected: - go sync(w) + // Before attempting to sync with our backend, + // we'll make sure that our birthday block has + // been set correctly to potentially prevent + // missing relevant events. + birthdayBlock, err := w.birthdaySanityCheck() + if err != nil { + err := fmt.Errorf("unable to sanity "+ + "check wallet birthday block: %v", + err) + log.Error(err) + panic(err) + } + + go sync(w, birthdayBlock) case chain.BlockConnected: err = walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error { return w.connectBlock(tx, wtxmgr.BlockMeta(n)) @@ -329,3 +344,189 @@ func (w *Wallet) addRelevantTx(dbtx walletdb.ReadWriteTx, rec *wtxmgr.TxRecord, return nil } + +// birthdaySanityCheck is a helper function that ensures our birthday block +// correctly reflects the birthday timestamp within a reasonable timestamp +// delta. It will be run after the wallet establishes its connection with the +// backend, but before it begins syncing. This is done as the second part to +// the wallet's address manager migration where we populate the birthday block +// to ensure we do not miss any relevant events throughout rescans. +func (w *Wallet) birthdaySanityCheck() (*waddrmgr.BlockStamp, error) { + // We'll start by acquiring our chain backend client as we'll be + // querying it for blocks. + chainClient, err := w.requireChainClient() + if err != nil { + return nil, err + } + + // We'll then fetch our wallet's birthday timestamp and block. + birthdayTimestamp := w.Manager.Birthday() + var birthdayBlock waddrmgr.BlockStamp + err = walletdb.View(w.db, func(tx walletdb.ReadTx) error { + var err error + ns := tx.ReadBucket(waddrmgrNamespaceKey) + birthdayBlock, err = w.Manager.BirthdayBlock(ns) + return err + }) + + switch { + // If our wallet's birthday block has not been set yet, then this is our + // initial sync, so we'll defer setting it until then. + case waddrmgr.IsError(err, waddrmgr.ErrBirthdayBlockNotSet): + return nil, nil + + // Otherwise, we'll return the error if there was one. + case err != nil: + return nil, err + } + + log.Debugf("Starting sanity check for the wallet's birthday block "+ + "from: height=%d, hash=%v", birthdayBlock.Height, + birthdayBlock.Hash) + + // Now, we'll need to determine if our block correctly reflects our + // timestamp. To do so, we'll fetch the block header and check its + // timestamp in the event that the birthday block's timestamp was not + // set (this is possible if it was set through the migration, since we + // do not store block timestamps). + candidate := birthdayBlock + header, err := chainClient.GetBlockHeader(&candidate.Hash) + if err != nil { + return nil, fmt.Errorf("unable to get header for block hash "+ + "%v: %v", candidate.Hash, err) + } + candidate.Timestamp = header.Timestamp + + // We'll go back a day worth of blocks in the chain until we find a + // block whose timestamp is below our birthday timestamp. + heightDelta := int32(144) + for birthdayTimestamp.Before(candidate.Timestamp) { + // If the birthday block has reached genesis, then we can exit + // our search as there exists no data before this point. + if candidate.Height == 0 { + break + } + + // To prevent requesting blocks out of range, we'll use a lower + // bound of the first block in the chain. + newCandidateHeight := int64(candidate.Height - heightDelta) + if newCandidateHeight < 0 { + newCandidateHeight = 0 + } + + // Then, we'll fetch the current candidate's hash and header to + // determine if it is valid. + hash, err := chainClient.GetBlockHash(newCandidateHeight) + if err != nil { + return nil, fmt.Errorf("unable to get block hash for "+ + "height %d: %v", candidate.Height, err) + } + header, err := chainClient.GetBlockHeader(hash) + if err != nil { + return nil, fmt.Errorf("unable to get header for "+ + "block hash %v: %v", candidate.Hash, err) + } + + candidate.Hash = *hash + candidate.Timestamp = header.Timestamp + + log.Debugf("Checking next birthday block candidate: "+ + "height=%d, hash=%v, timestamp=%v", + candidate.Height, candidate.Hash, + candidate.Timestamp) + } + + // To ensure we have a reasonable birthday block, we'll make sure it + // respects our birthday timestamp and it is within a reasonable delta. + // The birthday has already been adjusted to two days in the past of the + // actual birthday, so we'll make our expected delta to be within two + // hours of it to account for the network-adjusted time and prevent + // fetching more unnecessary blocks. + _, bestHeight, err := chainClient.GetBestBlock() + if err != nil { + return nil, err + } + timestampDelta := birthdayTimestamp.Sub(candidate.Timestamp) + for timestampDelta > 2*time.Hour { + // We'll determine the height for our next candidate and make + // sure it is not out of range. If it is, we'll lower our height + // delta until finding a height within range. + newHeight := candidate.Height + heightDelta + if newHeight > bestHeight { + heightDelta /= 2 + + // If we've exhausted all of our possible options at a + // later height, then we can assume the current birthday + // block is our best estimate. + if heightDelta == 0 { + break + } + + continue + } + + // We'll fetch the header for the next candidate and compare its + // timestamp. + hash, err := chainClient.GetBlockHash(int64(newHeight)) + if err != nil { + return nil, fmt.Errorf("unable to get block hash for "+ + "height %d: %v", candidate.Height, err) + } + header, err := chainClient.GetBlockHeader(hash) + if err != nil { + return nil, fmt.Errorf("unable to get header for "+ + "block hash %v: %v", hash, err) + } + + log.Debugf("Checking next birthday block candidate: "+ + "height=%d, hash=%v, timestamp=%v", newHeight, hash, + header.Timestamp) + + // If this block has exceeded our birthday timestamp, we'll look + // for the next candidate with a lower height delta. + if birthdayTimestamp.Before(header.Timestamp) { + heightDelta /= 2 + + // If we've exhausted all of our possible options at a + // later height, then we can assume the current birthday + // block is our best estimate. + if heightDelta == 0 { + break + } + + continue + } + + // Otherwise, this is a valid candidate, so we'll check to see + // if it meets our expected timestamp delta. + candidate.Hash = *hash + candidate.Height = newHeight + candidate.Timestamp = header.Timestamp + timestampDelta = birthdayTimestamp.Sub(header.Timestamp) + } + + // At this point, we've found a valid candidate that satisfies our + // conditions above. If this is our current birthday block, then we can + // exit to avoid the additional database transaction. + if candidate.Hash.IsEqual(&birthdayBlock.Hash) { + return &candidate, nil + } + + // Otherwise, we have a new, better candidate, so we'll write it to + // disk. + log.Debugf("Found a new valid wallet birthday block: height=%d, hash=%v", + candidate.Height, candidate.Hash) + + err = walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error { + ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) + if err := w.Manager.SetBirthdayBlock(ns, candidate); err != nil { + return err + } + return w.Manager.SetSyncedTo(ns, &candidate) + }) + if err != nil { + return nil, err + } + + return &candidate, nil +}