diff --git a/src/interfaces/chain.h b/src/interfaces/chain.h --- a/src/interfaces/chain.h +++ b/src/interfaces/chain.h @@ -138,12 +138,6 @@ findFirstBlockWithTimeAndHeight(int64_t time, int height, BlockHash *hash) = 0; - //! Return height of last block in the specified range which is pruned, - //! or nullopt if no block in the range is pruned. Range is inclusive. - virtual Optional - findPruned(int start_height = 0, - Optional stop_height = nullopt) = 0; - //! Return height of the highest block on the chain that is an ancestor //! of the specified block, or nullopt if no common ancestor is found. //! Also return the height of the specified block as an optional output @@ -212,6 +206,12 @@ //! the specified block hash are verified. virtual double guessVerificationProgress(const BlockHash &block_hash) = 0; + //! Return true if data is available for all blocks in the specified range + //! of blocks. This checks all blocks that are ancestors of block_hash in + //! the height range from min_height to max_height, inclusive. + virtual bool hasBlocks(const BlockHash &block_hash, int min_height = 0, + Optional max_height = {}) = 0; + //! Check if transaction has descendants in mempool. virtual bool hasDescendantsInMempool(const TxId &txid) = 0; diff --git a/src/interfaces/chain.cpp b/src/interfaces/chain.cpp --- a/src/interfaces/chain.cpp +++ b/src/interfaces/chain.cpp @@ -113,21 +113,6 @@ } return nullopt; } - Optional findPruned(int start_height, - Optional stop_height) override { - LockAssertion lock(::cs_main); - if (::fPruneMode) { - CBlockIndex *block = stop_height ? ::ChainActive()[*stop_height] - : ::ChainActive().Tip(); - while (block && block->nHeight >= start_height) { - if (block->nStatus.hasData() == 0) { - return block->nHeight; - } - block = block->pprev; - } - } - return nullopt; - } Optional findFork(const BlockHash &hash, Optional *height) override { LockAssertion lock(::cs_main); @@ -336,6 +321,29 @@ return GuessVerificationProgress(Params().TxData(), LookupBlockIndex(block_hash)); } + bool hasBlocks(const BlockHash &block_hash, int min_height, + Optional max_height) override { + // hasBlocks returns true if all ancestors of block_hash in + // specified range have block data (are not pruned), false if any + // ancestors in specified range are missing data. + // + // For simplicity and robustness, min_height and max_height are only + // used to limit the range, and passing min_height that's too low or + // max_height that's too high will not crash or change the result. + LOCK(::cs_main); + if (CBlockIndex *block = LookupBlockIndex(block_hash)) { + if (max_height && block->nHeight >= *max_height) { + block = block->GetAncestor(*max_height); + } + for (; block->nStatus.hasData(); block = block->pprev) { + // Check pprev to not segfault if min_height is too low + if (block->nHeight <= min_height || !block->pprev) { + return true; + } + } + } + return false; + } bool hasDescendantsInMempool(const TxId &txid) override { LOCK(::g_mempool.cs); auto it = ::g_mempool.GetIter(txid); diff --git a/src/qt/test/wallettests.cpp b/src/qt/test/wallettests.cpp --- a/src/qt/test/wallettests.cpp +++ b/src/qt/test/wallettests.cpp @@ -135,8 +135,8 @@ WalletRescanReserver reserver(*wallet); reserver.reserve(); CWallet::ScanResult result = wallet->ScanForWalletTransactions( - Params().GetConsensus().hashGenesisBlock, BlockHash(), reserver, - true /* fUpdate */); + Params().GetConsensus().hashGenesisBlock, {} /* max_height */, + reserver, true /* fUpdate */); QCOMPARE(result.status, CWallet::ScanResult::SUCCESS); QCOMPARE(result.last_scanned_block, ::ChainActive().Tip()->GetBlockHash()); diff --git a/src/test/interfaces_tests.cpp b/src/test/interfaces_tests.cpp --- a/src/test/interfaces_tests.cpp +++ b/src/test/interfaces_tests.cpp @@ -117,4 +117,39 @@ BOOST_CHECK_EQUAL(fork_hash, active[fork_height]->GetBlockHash()); } +BOOST_AUTO_TEST_CASE(hasBlocks) { + auto chain = interfaces::MakeChain(m_node, Params()); + auto &active = ChainActive(); + + // Test ranges + BOOST_CHECK(chain->hasBlocks(active.Tip()->GetBlockHash(), 10, 90)); + BOOST_CHECK(chain->hasBlocks(active.Tip()->GetBlockHash(), 10, {})); + BOOST_CHECK(chain->hasBlocks(active.Tip()->GetBlockHash(), 0, 90)); + BOOST_CHECK(chain->hasBlocks(active.Tip()->GetBlockHash(), 0, {})); + BOOST_CHECK(chain->hasBlocks(active.Tip()->GetBlockHash(), -1000, 1000)); + active[5]->nStatus = active[5]->nStatus.withData(false); + BOOST_CHECK(chain->hasBlocks(active.Tip()->GetBlockHash(), 10, 90)); + BOOST_CHECK(chain->hasBlocks(active.Tip()->GetBlockHash(), 10, {})); + BOOST_CHECK(!chain->hasBlocks(active.Tip()->GetBlockHash(), 0, 90)); + BOOST_CHECK(!chain->hasBlocks(active.Tip()->GetBlockHash(), 0, {})); + BOOST_CHECK(!chain->hasBlocks(active.Tip()->GetBlockHash(), -1000, 1000)); + active[95]->nStatus = active[95]->nStatus.withData(false); + BOOST_CHECK(chain->hasBlocks(active.Tip()->GetBlockHash(), 10, 90)); + BOOST_CHECK(!chain->hasBlocks(active.Tip()->GetBlockHash(), 10, {})); + BOOST_CHECK(!chain->hasBlocks(active.Tip()->GetBlockHash(), 0, 90)); + BOOST_CHECK(!chain->hasBlocks(active.Tip()->GetBlockHash(), 0, {})); + BOOST_CHECK(!chain->hasBlocks(active.Tip()->GetBlockHash(), -1000, 1000)); + active[50]->nStatus = active[50]->nStatus.withData(false); + BOOST_CHECK(!chain->hasBlocks(active.Tip()->GetBlockHash(), 10, 90)); + BOOST_CHECK(!chain->hasBlocks(active.Tip()->GetBlockHash(), 10, {})); + BOOST_CHECK(!chain->hasBlocks(active.Tip()->GetBlockHash(), 0, 90)); + BOOST_CHECK(!chain->hasBlocks(active.Tip()->GetBlockHash(), 0, {})); + BOOST_CHECK(!chain->hasBlocks(active.Tip()->GetBlockHash(), -1000, 1000)); + + // Test edge cases + BOOST_CHECK(chain->hasBlocks(active.Tip()->GetBlockHash(), 6, 49)); + BOOST_CHECK(!chain->hasBlocks(active.Tip()->GetBlockHash(), 5, 49)); + BOOST_CHECK(!chain->hasBlocks(active.Tip()->GetBlockHash(), 6, 50)); +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -3994,23 +3994,24 @@ } int start_height = 0; - BlockHash start_block, stop_block; + Optional stop_height; + BlockHash start_block; { auto locked_chain = pwallet->chain().lock(); - Optional tip_height = locked_chain->getHeight(); + LOCK(pwallet->cs_wallet); + int tip_height = pwallet->GetLastBlockHeight(); if (!request.params[0].isNull()) { start_height = request.params[0].get_int(); - if (start_height < 0 || !tip_height || start_height > *tip_height) { + if (start_height < 0 || start_height > tip_height) { throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid start_height"); } } - Optional stop_height; if (!request.params[1].isNull()) { stop_height = request.params[1].get_int(); - if (*stop_height < 0 || !tip_height || *stop_height > *tip_height) { + if (*stop_height < 0 || *stop_height > tip_height) { throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid stop_height"); } else if (*stop_height < start_height) { @@ -4021,29 +4022,21 @@ } // We can't rescan beyond non-pruned blocks, stop and throw an error - if (locked_chain->findPruned(start_height, stop_height)) { + if (!pwallet->chain().hasBlocks(pwallet->GetLastBlockHash(), + start_height, stop_height)) { throw JSONRPCError( RPC_MISC_ERROR, "Can't rescan beyond pruned data. Use RPC call " "getblockchaininfo to determine your pruned height."); } - if (tip_height) { - start_block = locked_chain->getBlockHash(start_height); - - // If called with a stop_height, set the stop_height here to - // trigger a rescan to that height. - // If called without a stop height, leave stop_height as null here - // so rescan continues to the tip (even if the tip advances during - // rescan). - if (stop_height) { - stop_block = locked_chain->getBlockHash(*stop_height); - } - } + CHECK_NONFATAL(pwallet->chain().findAncestorByHeight( + pwallet->GetLastBlockHash(), start_height, + FoundBlock().hash(start_block))); } CWallet::ScanResult result = pwallet->ScanForWalletTransactions( - start_block, stop_block, reserver, true /* fUpdate */); + start_block, stop_height, reserver, true /* fUpdate */); switch (result.status) { case CWallet::ScanResult::SUCCESS: break; diff --git a/src/wallet/test/wallet_tests.cpp b/src/wallet/test/wallet_tests.cpp --- a/src/wallet/test/wallet_tests.cpp +++ b/src/wallet/test/wallet_tests.cpp @@ -59,7 +59,7 @@ WalletRescanReserver reserver(wallet); reserver.reserve(); CWallet::ScanResult result = wallet.ScanForWalletTransactions( - BlockHash(), BlockHash(), reserver, false /* update */); + BlockHash(), {} /* max_height */, reserver, false /* update */); BOOST_CHECK_EQUAL(result.status, CWallet::ScanResult::SUCCESS); BOOST_CHECK(result.last_failed_block.IsNull()); BOOST_CHECK(result.last_scanned_block.IsNull()); @@ -81,7 +81,8 @@ WalletRescanReserver reserver(wallet); reserver.reserve(); CWallet::ScanResult result = wallet.ScanForWalletTransactions( - oldTip->GetBlockHash(), BlockHash(), reserver, false /* update */); + oldTip->GetBlockHash(), {} /* max_height */, reserver, + false /* update */); BOOST_CHECK_EQUAL(result.status, CWallet::ScanResult::SUCCESS); BOOST_CHECK(result.last_failed_block.IsNull()); BOOST_CHECK_EQUAL(result.last_scanned_block, newTip->GetBlockHash()); @@ -107,7 +108,8 @@ WalletRescanReserver reserver(wallet); reserver.reserve(); CWallet::ScanResult result = wallet.ScanForWalletTransactions( - oldTip->GetBlockHash(), BlockHash(), reserver, false /* update */); + oldTip->GetBlockHash(), {} /* max_height */, reserver, + false /* update */); BOOST_CHECK_EQUAL(result.status, CWallet::ScanResult::FAILURE); BOOST_CHECK_EQUAL(result.last_failed_block, oldTip->GetBlockHash()); BOOST_CHECK_EQUAL(result.last_scanned_block, newTip->GetBlockHash()); @@ -132,7 +134,8 @@ WalletRescanReserver reserver(wallet); reserver.reserve(); CWallet::ScanResult result = wallet.ScanForWalletTransactions( - oldTip->GetBlockHash(), BlockHash(), reserver, false /* update */); + oldTip->GetBlockHash(), {} /* max_height */, reserver, + false /* update */); BOOST_CHECK_EQUAL(result.status, CWallet::ScanResult::FAILURE); BOOST_CHECK_EQUAL(result.last_failed_block, newTip->GetBlockHash()); BOOST_CHECK(result.last_scanned_block.IsNull()); @@ -512,8 +515,8 @@ WalletRescanReserver reserver(*wallet); reserver.reserve(); CWallet::ScanResult result = wallet->ScanForWalletTransactions( - ::ChainActive().Genesis()->GetBlockHash(), BlockHash(), reserver, - false /* update */); + ::ChainActive().Genesis()->GetBlockHash(), {} /* max_height */, + reserver, false /* update */); BOOST_CHECK_EQUAL(result.status, CWallet::ScanResult::SUCCESS); BOOST_CHECK_EQUAL(result.last_scanned_block, ::ChainActive().Tip()->GetBlockHash()); diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -1069,7 +1069,7 @@ BlockHash last_failed_block; }; ScanResult ScanForWalletTransactions(const BlockHash &first_block, - const BlockHash &last_block, + Optional max_height, const WalletRescanReserver &reserver, bool fUpdate); void TransactionRemovedFromMempool(const CTransactionRef &ptx) override; diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -1720,8 +1720,8 @@ if (start) { // TODO: this should take into account failure by ScanResult::USER_ABORT - ScanResult result = ScanForWalletTransactions(start_block, BlockHash(), - reserver, update); + ScanResult result = ScanForWalletTransactions( + start_block, {} /* max_height */, reserver, update); if (result.status == ScanResult::FAILURE) { int64_t time_max; CHECK_NONFATAL(chain().findBlock(result.last_failed_block, @@ -1739,9 +1739,8 @@ * * @param[in] start_block Scan starting block. If block is not on the active * chain, the scan will return SUCCESS immediately. - * @param[in] stop_block Scan ending block. If block is not on the active - * chain, the scan will continue until it reaches the - * chain tip. + * @param[in] max_height Optional max scanning height. If unset there is + * no maximum and scanning can continue to the tip * * @return ScanResult returning scan information and indicating success or * failure. Return status will be set to SUCCESS if scan was @@ -1754,7 +1753,7 @@ * transactions for. */ CWallet::ScanResult CWallet::ScanForWalletTransactions( - const BlockHash &start_block, const BlockHash &stop_block, + const BlockHash &start_block, Optional max_height, const WalletRescanReserver &reserver, bool fUpdate) { int64_t nNow = GetTime(); int64_t start_time = GetTimeMillis(); @@ -1785,9 +1784,13 @@ tip_hash = locked_chain->getBlockHash(*tip_height); } block_height = locked_chain->getBlockHeight(block_hash); + BlockHash end_hash = tip_hash; + if (max_height) { + chain().findAncestorByHeight(tip_hash, *max_height, + FoundBlock().hash(end_hash)); + } progress_begin = chain().guessVerificationProgress(block_hash); - progress_end = chain().guessVerificationProgress( - stop_block.IsNull() ? tip_hash : stop_block); + progress_end = chain().guessVerificationProgress(end_hash); } double progress_current = progress_begin; while (block_height && !fAbortRescan && !chain().shutdownRequested()) { @@ -1837,7 +1840,7 @@ result.last_failed_block = block_hash; result.status = ScanResult::FAILURE; } - if (block_hash == stop_block) { + if (max_height && *block_height >= *max_height) { break; } { @@ -1857,7 +1860,7 @@ // handle updated tip hash const BlockHash prev_tip_hash = tip_hash; tip_hash = locked_chain->getBlockHash(*tip_height); - if (stop_block.IsNull() && prev_tip_hash != tip_hash) { + if (!max_height && prev_tip_hash != tip_hash) { // in case the tip has changed, update progress max progress_end = chain().guessVerificationProgress(tip_hash); } @@ -4424,8 +4427,8 @@ (ScanResult::SUCCESS != walletInstance ->ScanForWalletTransactions( - locked_chain->getBlockHash(rescan_height), BlockHash(), - reserver, true /* update */) + locked_chain->getBlockHash(rescan_height), + {} /* max_height */, reserver, true /* update */) .status)) { error = _("Failed to rescan the wallet during initialization"); return nullptr;