Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

Already on GitHub? Sign in to your account

[Wallet] Add RPC call "rescanblockchain <startheight> <stopheight>" #7061

Merged
merged 2 commits into from Oct 13, 2017

Conversation

Member

jonasschnelli commented Nov 19, 2015

A RPC rescan command is much more flexible for the following reasons:

  • You can define the start and end-height
  • It can be called during runtime
  • It can work in multiwallet environment
Owner

laanwj commented Nov 19, 2015

I'd rather have it that the API is such that an explicit rescan is never needed. Wasn't there some work on a multi-import w/ timestamps?

Member

jonasschnelli commented Nov 19, 2015

Yes. There is a PR (see PR description). I agree that it would be better to avoid rescans at all, although it might be complicated to catch all edge-cases and a manual trigger can help in situations where someone needs to deal with multiple/complex imports (you only want to do one rescan).

And I think some people will cancel a rescan because they want to do other stuff and/or had not considered that a rescan can take a long time. Afterward calling -rescan (from genesis) is a time consuming option.

Owner

laanwj commented Nov 19, 2015

But manually specifying a block # to rescan from is extremely fragile... it's very easy to get this wrong.

Also, rescanning doesn't interact with pruning which will be more and more common in the future.

Member

gmaxwell commented Nov 19, 2015

@lannwj I thought thats part of what the height parameter here was for-- addressing pruning comparability?

Member

petertodd commented Nov 20, 2015

How about we call this rescanfromheight instead, to make it possible to add a rescanfromtime later if users demand? Equally, some kind of RPC call that finds the first block with a nTime after a specific time might be useful here.

Do we have a way of querying what block # is the oldest non-pruned block?

Member

petertodd commented Nov 20, 2015

It also occurs to me that for this usecase we might instead want to have pruning not happen automatically, but rather be an on-demand thing where the user specifies the oldest time they're interested in.

Member

gmaxwell commented Nov 20, 2015

So the biggest negative I personally see here is that it furthers this misunderstanding that rescan is some thing users generally need to be doing. Until we added these non-rescan imports a user initiated rescan is something that never should have been needed (and indicated a serious bug we'd like to know about if it was). As a result of the -rescan argument there is now this whole cargo cult of people that rescan every time they're scarred by a shadow. I hate to further that.

But I can't deny how really useful this will be.

Member

petertodd commented Nov 21, 2015

@gmaxwell A possible way around that would be to make the rescan check if you have any addresses that haven't yet been scanned in that range and error out if not. (basically make it say "no rescan needed")

Member

gmaxwell commented Nov 22, 2015

Hm. this could also take a stop argument, allowing you to scan single blocks or avoid rescan overlap. Also, I think all the wallet re-scanning should traverse its interval backwards-- for more instant gratification; though this would dork with the wallet transaction ordering... actually import at all breaks that, I should go talk to luke-jr about that.

@promag promag and 1 other commented on an outdated diff Nov 23, 2015

src/wallet/rpcdump.cpp
+ + HelpExampleRpc("rescanblockchain", "\"100000\"")
+ );
+
+ LOCK2(cs_main, pwalletMain->cs_wallet);
+
+ CBlockIndex *pIndexRescan = NULL;
+ if (params.size() > 0 && params[0].isNum())
+ pIndexRescan = chainActive[params[0].get_int()];
+
+ if (!pIndexRescan)
+ pIndexRescan = chainActive.Genesis();
+
+ //We can't rescan beyond non-pruned blocks, stop and throw an error
+ if (fPruneMode)
+ {
+ if (fPruneMode)
@promag

promag Nov 23, 2015

Contributor

Remove?

@jonasschnelli

jonasschnelli Nov 24, 2015

Member

Oops. A rebase issue. Thanks for point out. Fixed.

Contributor

pstratem commented Nov 24, 2015

agree with gmaxwell that this should scan a range of blocks

Member

jonasschnelli commented Nov 24, 2015

Agree with the stop parameter. Working on a implementation....

Owner

sipa commented Nov 24, 2015

I would still prefer an approach that imports with birthdate instead of explicit rescanning.

Member

jonasschnelli commented Nov 24, 2015

Added a commit that allows providing a optional parameter with a height where the rescan should stop.

@sipa: I agree that rescan height over a key/address birthday would be nice to have (see #6570). But a explicit rescan RPC call can be useful IMO. It's trivial to maintain and it can save lots of rescan-time on the user side. But agree, it has to be considered as "experts" feature.

What about implementing a threshold for autodetecting wether the parameter is a blockheight or timestamp (similar to LOCKTIME_THRESHOLD)?

Contributor

promag commented Nov 24, 2015

@jonasschnelli see #6570 (comment) regarding your last comment.

GIJensen commented Dec 7, 2015

ACK

Contributor

mrbandrews commented Mar 14, 2016

If this is still moving forward - I tested it a bit (including pruned mode) and it looks fine to me. One suggestion is to make the start-height a required parameter so that the user specifies "rescanblockchain 1" to scan from genesis. If the start-height is < 1 or higher than current height, throw an error.
Otherwise, ACK.

@promag promag commented on an outdated diff May 2, 2016

src/wallet/wallet.cpp
@@ -1084,6 +1087,8 @@ int CWallet::ScanForWalletTransactions(CBlockIndex* pindexStart, bool fUpdate)
if (AddToWalletIfInvolvingMe(tx, &block, fUpdate))
ret++;
}
+ if (pindex == pindexStop)
@promag

promag May 2, 2016

Contributor

Move to while condition?

@promag

promag May 2, 2016

Contributor

Because it's inclusive?

@laanwj laanwj added the Feature label Jun 16, 2016

luke-jr added a commit to bitcoinknots/bitcoin that referenced this pull request Jun 28, 2016

Member

jonasschnelli commented Jul 20, 2016

Rebased.
I think there are still reasons to consider that PR. At the moment, it would really be useful. Even once we have #7551 (importmulti) it could see use cases for rescanblockchain.

Owner

sipa commented Aug 25, 2016

This seems better #7984, but I still prefer not furthering the usage of various rescans. We need APIs that don't require users to keep track of the concept of rescanning IMHO.

Member

jonasschnelli commented Aug 25, 2016

I agree. Ideally, there will be no need to rescan. But in practice, rescans are sometimes required (I guess everyone who gave some users support has encountered that). IMO a rpc rescan commend with an optional hight is much more flexible then -rescan as a startup argument.

Owner

sipa commented Aug 25, 2016

I think we should work on importmulti instead (or at least an importprivkey that takes a key birthdate as parameter), not on more ways to rescan.

Member

jonasschnelli commented Aug 25, 2016

I think we should work on importmulti instead (or at least an importprivkey that takes a key birthdate as parameter), not on more ways to rescan.

Yes. I can agree with that.

Member

jonasschnelli commented Sep 14, 2016

Another thing where this could be useful is restoring hd wallets

Member

MarcoFalke commented Oct 29, 2016

Since importmulti #7551 is merged, some use cases are covered by that.

Owner

laanwj commented Nov 2, 2016

I think we should work on importmulti instead (or at least an importprivkey that takes a key birthdate as parameter), not on more ways to rescan.

Tend to agree. Closing this for now.

@laanwj laanwj closed this Nov 2, 2016

Member

jonasschnelli commented Dec 22, 2016

IMO something like that would be very handy if you rescan a HD wallet (old backup).
-rescan does not allow direct user feedback
IMO rescanning an old HD backup should by default start at the height where we introduces HD (optional down to genesis).

@jonasschnelli jonasschnelli reopened this May 24, 2017

@jonasschnelli jonasschnelli changed the title from [Wallet] add rescanblockchain <height> RPC command to [Wallet] Replace -rescan with a new RPC call "rescanblockchain" May 24, 2017

Member

jonasschnelli commented May 24, 2017

Reopened and overhauled.
This now does replace the -rescan startup argument with a new RPC call rescanblockchain. The reasons for that are:

  • You can define the start and end-height
  • It can be called during runtime
  • It can work in multiwallet environment

Using -rescan will cancel the startup with an error referring to the new RPC call.

Should add some test coverage for this. One easy way might be to add a new Rescan.manual enum value here:

Rescan = enum.Enum("Rescan", "no yes late_timestamp")
, then put

if self.rescan == Rescan.manual:
    self.node.rescanblockchain()

at the end of the do_import method.

doc/man/bitcoind.1
@@ -90,7 +90,7 @@ Reduce storage requirements by enabling pruning (deleting) of old
blocks. This allows the pruneblockchain RPC to be called to
delete specific blocks, and enables automatic pruning of old
blocks if a target size in MiB is provided. This mode is
-incompatible with \fB\-txindex\fR and \fB\-rescan\fR. Warning: Reverting this
+incompatible with \fB\-txindex\fR. Warning: Reverting this
@ryanofsky

ryanofsky May 24, 2017

Contributor

I think these files are automatically generated from -help output and could be reverted in the PR.

src/init.cpp
@@ -367,7 +367,7 @@ std::string HelpMessage(HelpMessageMode mode)
#ifndef WIN32
strUsage += HelpMessageOpt("-pid=<file>", strprintf(_("Specify pid file (default: %s)"), BITCOIN_PID_FILENAME));
#endif
- strUsage += HelpMessageOpt("-prune=<n>", strprintf(_("Reduce storage requirements by enabling pruning (deleting) of old blocks. This allows the pruneblockchain RPC to be called to delete specific blocks, and enables automatic pruning of old blocks if a target size in MiB is provided. This mode is incompatible with -txindex and -rescan. "
+ strUsage += HelpMessageOpt("-prune=<n>", strprintf(_("Reduce storage requirements by enabling pruning (deleting) of old blocks. This allows the pruneblockchain RPC to be called to delete specific blocks, and enables automatic pruning of old blocks if a target size in MiB is provided. This mode is incompatible with -txindex"
@ryanofsky

ryanofsky May 24, 2017

Contributor

Maybe still worth mentioning that pruning can get in the way of successfully importing wallet keys & rescanning.

src/wallet/rpcdump.cpp
+ "rescanblockchain (\"start-height\") (\"stop-height\")\n"
+ "\nRescan the local blockchain for wallet related transactions.\n"
+ "\nArguments:\n"
+ "1. \"start-height\" (number, optional) blockheight where the rescan should start\n"
@ryanofsky

ryanofsky May 24, 2017

Contributor

Maybe this should take either times or heights like the pruneblockchain RPC:

"1. \"height\" (numeric, required) The block height to prune up to. May be set to a discrete height, or a unix timestamp\n"

@jnewbery

jnewbery May 24, 2017

Member

suggest you change the argument names to startheight and stopheight. No other rpcs use - as word delimiter.

@jonasschnelli

jonasschnelli May 25, 2017

Member

Indeed. Fixed.

src/wallet/rpcdump.cpp
+ pIndexStart = chainActive.Genesis();
+
+ //We can't rescan beyond non-pruned blocks, stop and throw an error
+ if (fPruneMode)
@ryanofsky

ryanofsky May 24, 2017

Contributor

Style might need to be updated for this code (moving opening brace to same line here, adding missing braces other lines)

src/wallet/rpcdump.cpp
+ block = block->pprev;
+
+ if (pIndexStart->nHeight < block->nHeight)
+ throw JSONRPCError(RPC_WALLET_ERROR, "Can't rescan beyond pruned data.");
@ryanofsky

ryanofsky May 24, 2017

Contributor

Might be helpful if error message could mention getblockchaininfo RPC for getting pruned height.

src/wallet/wallet.cpp
@@ -1508,6 +1512,9 @@ CBlockIndex* CWallet::ScanForWalletTransactions(CBlockIndex* pindexStart, bool f
} else {
ret = nullptr;
}
+ if (pindex == pindexStop) {
@ryanofsky

ryanofsky May 24, 2017

Contributor

I don't understand what the use-case is for the stop argument. Can you describe a scenario where you would want to provide it? I can see how it might have been desirable before there was an abortrescan RPC to break a big scan up into little scans, but I don't see why you'd want it now.

@jonasschnelli

jonasschnelli May 24, 2017

Member

See the discussion about the stop argument: #7061 (comment)

src/wallet/wallet.cpp
@@ -3865,7 +3871,7 @@ CWallet* CWallet::CreateWalletFromFile(const std::string walletFile)
RegisterValidationInterface(walletInstance);
CBlockIndex *pindexRescan = chainActive.Genesis();
- if (!GetBoolArg("-rescan", false))
@ryanofsky

ryanofsky May 24, 2017

Contributor

Is there any advantage to getting rid of the -rescan option if we still have keep all the rescan logic below? Is there a future cleanup or new feature that will be easier to implement with the option gone?

@jonasschnelli

jonasschnelli May 24, 2017

Member

The -rescan option is global while this PR changes it (partially) to per-wallet. Still, -zapwallettx, etc. need to also be moved to per-wallet basis (I don't know how right now).

But removing the global -rescan option has to be done sooner or later if we want proper multiwallet.

@ryanofsky

ryanofsky May 24, 2017

Contributor

But removing the global -rescan option has to be done sooner or later if we want proper multiwallet.

Do -zapwallettx, etc also need to be removed to support proper multiwallet?

@jonasschnelli

jonasschnelli May 24, 2017

Member

IMO Yes. It's pure db utility. Either we move it to RPC or to a new wallet/db tool.

@jnewbery

jnewbery May 24, 2017

Member

This is confusing: the if statement requires !needsRescan. Shouldn't that be the other way around? If needsRescan is true, we should rescan. I think the if statement should be:

if (needsRescan || GetBoolArg("-salvagewallet", true) || GetBoolArg("-zapwallettxes", true))

re: zapwallet and salvagewallet. I agree that they should be changed to RPCs for multiwallet.

@jonasschnelli

jonasschnelli May 25, 2017

Member

No. I don't think so.
If that if statement is true, we load the wallet best block which prevents a complete rescan (so it's the opposite).
So we only load the wallet best block (== no rescan) if needsRescan is false and -salvagewallet and --zapwallettxes` have not been set.

@jnewbery

jnewbery May 25, 2017

Member

Yes, you're right.

src/wallet/db.cpp
@@ -167,8 +167,8 @@ bool CDB::Recover(const std::string& filename, void *callbackDataIn, bool (*reco
// Call Salvage with fAggressive=true to
// get as much data as possible.
// Rewrite salvaged data to fresh wallet file
- // Set -rescan so any missing transactions will be
- // found.
+ // call rescanblockchain (RPC) so any missing
@jnewbery

jnewbery May 24, 2017

Member

I can't see where rescanblockchain() is being called. Am I missing something?

@jonasschnelli

jonasschnelli May 25, 2017

Member

It happens outside of this call (somewhere in wallet.cpp). But the comment doesn't say "It" calls rescanblockchain, it either says "you" can call rescanblockchain().

@jnewbery

jnewbery May 25, 2017

Member

I don't understand. The comment is:

    // Recovery procedure:
    // move wallet file to wallet.timestamp.bak
    // Call Salvage with fAggressive=true to
    // get as much data as possible.
    // Rewrite salvaged data to fresh wallet file
    // call rescanblockchain (RPC) so any missing
    // transactions will be found.

This function does:

  • move wallet file to wallet.timestamp.bak
  • Call Salvage with fAggressive=true to get as much data as possible.
  • Rewrite salvaged data to fresh wallet file

but it doesn't call rescanblockchain, and I can't see rescanblockchain being called by the callers.

@jonasschnelli

jonasschnelli May 25, 2017

Member

Yes. You right. The rescan is not part of the call. But the part that calls CDB::Recover does always call a rescan. But now I got your point. We should refer to ScanForWalletTransactions() instead to rescanblockchain (RPC). Right?

@jnewbery

jnewbery May 25, 2017

Member

sorry, I'm still not seeing it. You say "the part that calls CDB::Recover does always call a rescan". Where?

@jonasschnelli

jonasschnelli May 25, 2017

Member

It's confusing. But CDB::Recover only gets calls by setting -salvagewallet which does set -rescan. Maybe I should remove that comment part... I don't know what's best because -rescan is currently mentioned there.

@jnewbery

jnewbery May 25, 2017

Member

ah, makes sense now. Thank you. I wasn't looking in init.cpp, but now I think I see how this fits together. CWallet::Verify() is called first, which calls CWalletDB::Recover() if -salvagewallet is set. Later in AppInitMain(), we call CWallet::AppInitMain(), which is where the rescan happens if -salvagewallet is set.

Perhaps change the comment to something like:

    // Try to recover the wallet:
    // - move wallet file to wallet.timestamp.bak
    // - call Salvage with fAggressive=true to get as much data as possible.
    // - rewrite salvaged data to fresh wallet file
    //
    // CWallet::AppInitMain() will later run a full blockchain rescan to find
    // any missing transactions.
src/wallet/rpcdump.cpp
+ "rescanblockchain (\"start-height\") (\"stop-height\")\n"
+ "\nRescan the local blockchain for wallet related transactions.\n"
+ "\nArguments:\n"
+ "1. \"start-height\" (number, optional) blockheight where the rescan should start\n"
@jnewbery

jnewbery May 24, 2017

Member

suggest you change the argument names to startheight and stopheight. No other rpcs use - as word delimiter.

src/wallet/rpcdump.cpp
+
+ CBlockIndex *pIndexStart = NULL;
+ CBlockIndex *pIndexStop = NULL;
+ if (request.params.size() > 0 && request.params[0].isNum())
@jnewbery

jnewbery May 24, 2017

Member

This should throw an error if the argument isn't a number, rather than continue. It should allow the argument to be Null. I think the if test you need is:

if (request.params.size() > 0 && !request.params[0].isNull()) {
src/wallet/rpcdump.cpp
+ if (request.params.size() > 0 && request.params[0].isNum())
+ pIndexStart = chainActive[request.params[0].get_int()];
+
+ if (request.params.size() > 1 && request.params[1].isNum())
@jnewbery

jnewbery May 24, 2017

Member

should this return an error if heightstop is lower than heightstart? If heightstop is higher than the tip height?

src/wallet/rpcdump.cpp
+ if (pwallet)
+ pwallet->ScanForWalletTransactions(pIndexStart, pIndexStop, true);
+
+ return NullUniValue;
@jnewbery

jnewbery May 24, 2017

Member

This RPC should return a message to the user if it is successful, eg "Blockchain rescanned from block <hash> height <height> to block <hash> height <height>. <numtx> transactions added to wallet". Otherwise it's impossible for the user to know whether the call was successful or not.

@ryanofsky

ryanofsky Jul 25, 2017

Contributor

Should at least throw an error this isn't successful (the ScanForWalletTransactions call will return null on success, otherwise a pointer to the last failing block, so this should be pretty easy).

src/wallet/wallet.cpp
@@ -1468,11 +1468,15 @@ void CWalletTx::GetAmounts(std::list<COutputEntry>& listReceived,
* before CWallet::nTimeFirstKey). Returns null if there is no such range, or
@jnewbery

jnewbery May 24, 2017

Member

nit: update function comment to say that the rescan is up to pindexStop if it is non-null

src/wallet/wallet.cpp
@@ -1485,7 +1489,7 @@ CBlockIndex* CWallet::ScanForWalletTransactions(CBlockIndex* pindexStart, bool f
while (pindex && nTimeFirstKey && (pindex->GetBlockTime() < (nTimeFirstKey - TIMESTAMP_WINDOW)))
pindex = chainActive.Next(pindex);
- ShowProgress(_("Rescanning..."), 0); // show rescan progress in GUI as dialog or on splashscreen, if -rescan on startup
+ ShowProgress(_("Rescanning..."), 0); // show rescan progress in GUI as dialog or on splashscreen
@jnewbery

jnewbery May 24, 2017

Member

Need to remove more of this comment, so that it just reads:

// show rescan progress in GUI as dialog

@jonasschnelli

jonasschnelli May 25, 2017

Member

But can't it also happens during startup when the GUI will show the progress only in the splash-screen and not in a dialog?

src/wallet/wallet.cpp
@@ -3865,7 +3871,7 @@ CWallet* CWallet::CreateWalletFromFile(const std::string walletFile)
RegisterValidationInterface(walletInstance);
CBlockIndex *pindexRescan = chainActive.Genesis();
- if (!GetBoolArg("-rescan", false))
@jnewbery

jnewbery May 24, 2017

Member

This is confusing: the if statement requires !needsRescan. Shouldn't that be the other way around? If needsRescan is true, we should rescan. I think the if statement should be:

if (needsRescan || GetBoolArg("-salvagewallet", true) || GetBoolArg("-zapwallettxes", true))

re: zapwallet and salvagewallet. I agree that they should be changed to RPCs for multiwallet.

luke-jr added a commit to bitcoinknots/bitcoin that referenced this pull request Jun 15, 2017

Contributor

TheBlueMatt commented Jul 11, 2017

This needs rebase (probably just wait till after 15 at this point). I think outstanding objections to the idea have all largely been removed, Concept ACK from me, at least.

Member

jonasschnelli commented Jul 21, 2017

Rebased.

utACK 7858b9f, but I would prefer if this just deprecated the -rescan option instead of removing it. Removing it with no warning seems needless and kind of jerky, since it barely saves any code.

src/wallet/rpcdump.cpp
+ "rescanblockchain (\"startheight\") (\"stopheight\")\n"
+ "\nRescan the local blockchain for wallet related transactions.\n"
+ "\nArguments:\n"
+ "1. \"startHeight\" (number, optional) blockheight where the rescan should start\n"
@ryanofsky

ryanofsky Jul 25, 2017

Contributor

Capitalization of height doesn't match actual param name.

src/wallet/rpcdump.cpp
+ "\nRescan the local blockchain for wallet related transactions.\n"
+ "\nArguments:\n"
+ "1. \"startHeight\" (number, optional) blockheight where the rescan should start\n"
+ "2. \"stopHeight\" (number, optional) blockheight where the rescan should stop\n"
@ryanofsky

ryanofsky Jul 25, 2017

Contributor

Could do what pruneblockchain does and allow height to be specified either as a physical height or a time:

"1. \"height\" (numeric, required) The block height to prune up to. May be set to a discrete height, or a unix timestamp\n"

src/wallet/rpcdump.cpp
+
+ CBlockIndex *pIndexStart = NULL;
+ CBlockIndex *pIndexStop = NULL;
+ if (request.params.size() > 0 && request.params[0].isNum()) {
@ryanofsky

ryanofsky Jul 25, 2017

Contributor

Size checks can be dropped here and below. params[0] will return a null value if the param is missing.

src/wallet/rpcdump.cpp
+ //We can't rescan beyond non-pruned blocks, stop and throw an error
+ if (fPruneMode) {
+ CBlockIndex *block = chainActive.Tip();
+ while (block && block->pprev && (block->pprev->nStatus & BLOCK_HAVE_DATA)) {
@ryanofsky

ryanofsky Jul 25, 2017

Contributor

This should check against pIndexStop too, so error won't trigger unnecessarily in cases where missing blocks are noncontinuous (which can happen with manual pruning).

src/wallet/rpcdump.cpp
+ if (pwallet)
+ pwallet->ScanForWalletTransactions(pIndexStart, pIndexStop, true);
+
+ return NullUniValue;
@ryanofsky

ryanofsky Jul 25, 2017

Contributor

Should at least throw an error this isn't successful (the ScanForWalletTransactions call will return null on success, otherwise a pointer to the last failing block, so this should be pretty easy).

Contributor

promag commented Jul 27, 2017

If #10941 goes first then the test can be extended to do a rescanblockchain and assert the notified blocks.

Member

luke-jr commented Aug 11, 2017

Rebased and addressed @ryanofsky 's comments on my rpc_rescan branch.

Member

jonasschnelli commented Aug 11, 2017

Cherry picked @luke-jr rebased / overhauled version.
Still needs an RPC test.

Need to change the progress calculation:

         double dProgressTip = GuessVerificationProgress(chainParams.TxData(), chainActive.Tip());

Should rename to dProgressStop.

src/wallet/rpcdump.cpp
+
+ if (request.fHelp || request.params.size() > 2) {
+ throw std::runtime_error(
+ "rescanblockchain (\"startheight\") (\"stopheight\")\n"
@promag

promag Aug 12, 2017

Contributor

Remove inner ) (.

@luke-jr

luke-jr Aug 12, 2017

Member

No... they're both independently optional.

@promag

promag Aug 12, 2017

Contributor

But without named args that's not possible.

@luke-jr

luke-jr Aug 12, 2017

Member

Yes it is...

@promag

promag Aug 12, 2017

Contributor

How?

@luke-jr

luke-jr Aug 12, 2017

Member

JSON null is equivalent to omitting a parameter.

@promag

promag Aug 12, 2017

Contributor

What I mean is that with bitcoin-cli rescanblockchain 10 10 will always be startheight and to specify only stopheight you have to call bitcoin-cli rescanblockchain null 10, hence stopheight depends on startheight.

@luke-jr

luke-jr Aug 12, 2017

Member

Not sure what you're getting at. Null is omitted.

src/wallet/rpcdump.cpp
+ "rescanblockchain (\"startheight\") (\"stopheight\")\n"
+ "\nRescan the local blockchain for wallet related transactions.\n"
+ "\nArguments:\n"
+ "1. \"startheight\" (number, optional) blockheight where the rescan should start\n"
@promag

promag Aug 12, 2017

Contributor

Just start?

Also, if positive then height, if negative then depth relative to tip. Same for stop.

@jnewbery

jnewbery Sep 12, 2017

Member

if positive then height, if negative then depth relative to tip

In general we don't like overloading arguments with multiple meanings in this way (I know - I've tried to do it myself before and got NACKed.

The argument names should be changed to start_height and stop_height to follow style guidelines. I know that this PR was opened before the style changes so I won't suggest that you change variable names, but this is a public interface so it should adhere to the new guidelines.

src/wallet/rpcdump.cpp
+ "\nRescan the local blockchain for wallet related transactions.\n"
+ "\nArguments:\n"
+ "1. \"startheight\" (number, optional) blockheight where the rescan should start\n"
+ "2. \"stopheight\" (number, optional) blockheight where the rescan should stop\n"
@promag

promag Aug 12, 2017

Contributor

Note that it's inclusive?

src/wallet/rpcdump.cpp
+
+ LOCK2(cs_main, pwallet->cs_wallet);
+
+ CBlockIndex *pindexStart = NULL;
@promag

promag Aug 12, 2017

Contributor

pindex_start?

src/wallet/rpcdump.cpp
+
+ CBlockIndex *pindexStart = NULL;
+ CBlockIndex *pindexStop = NULL;
+ if (request.params[0].isNum()) {
@promag

promag Aug 12, 2017

Contributor

IMO if (!request.params[0].isNull()) so that get_int throws if invalid value is supplied.

src/wallet/rpcdump.cpp
+ pindexStart = chainActive.Genesis();
+ }
+
+ if (request.params[1].isNum()) {
@promag

promag Aug 12, 2017

Contributor

Same as above.

src/wallet/rpcdump.cpp
+
+ if (request.params[1].isNum()) {
+ pindexStop = chainActive[request.params[1].get_int()];
+ }
@promag

promag Aug 12, 2017

Contributor

Maybe we should } else { pindex_stop = chainActive.Tip(); }?

@jonasschnelli

jonasschnelli Sep 5, 2017

Member

The stop height can just be a nullptr (== scan up to the tip).

src/wallet/rpcdump.cpp
+ }
+
+ if (pindexStop && pindexStop->nHeight < pindexStart->nHeight) {
+ // Flip the parameters to the expected order
@promag

promag Aug 12, 2017

Contributor

Should be an error instead?

@promag

promag Aug 12, 2017

Contributor

Why would start be greater than stop? Seems to be a bad call?

@luke-jr

luke-jr Aug 12, 2017

Member

It doesn't make much less sense to scan backward than to scan forward. We only actually support scanning forward, but the outcome is the same either way.

@promag

promag Aug 12, 2017

Contributor

I see your point 👍.

src/wallet/rpcdump.cpp
+
+ if (pindexStop && pindexStop->nHeight < pindexStart->nHeight) {
+ // Flip the parameters to the expected order
+ CBlockIndex * const tmp = pindexStart;
@promag

promag Aug 12, 2017

Contributor

Use std::swap?

src/wallet/rpcdump.cpp
+ // We can't rescan beyond non-pruned blocks, stop and throw an error
+ if (fPruneMode) {
+ int nHeight = pindexStart->nHeight;
+ while ((!pindexStop) || nHeight <= pindexStop->nHeight) {
@promag

promag Aug 12, 2017

Contributor

Correct me if I'm wrong, but if start has BLOCK_HAVE_DATA then the remaining blocks also have, therefore no need to loop?

@luke-jr

luke-jr Aug 12, 2017

Member

I think this may be currently true, but perhaps not safe to assume for the future. Unsure.

@promag

promag Aug 12, 2017

Contributor

Maybe factor out to a CChain function so in the future it's more contained and for now remove the loop?

@jonasschnelli

jonasschnelli Sep 5, 2017

Member

We loop backwards here which the check is necessary. Or am I wrong?

@promag

promag Sep 6, 2017

Contributor

Something is wrong now because nothing changes the the loop exit conditions.

src/wallet/wallet.cpp
{
int64_t nNow = GetTime();
const CChainParams& chainParams = Params();
+ if (pindexStop && pindexStop->nHeight < pindexStart->nHeight) {
@promag

promag Aug 12, 2017

Contributor

Should not happen, assert instead?

@jonasschnelli

jonasschnelli Oct 4, 2017

Member

Can happen from the perspective of the function. But maybe not with the current consumption of the function.

src/wallet/rpcdump.cpp
+ pwallet->ScanForWalletTransactions(pindexStart, pindexStop, true);
+ }
+
+ return NullUniValue;
@promag

promag Aug 12, 2017

Contributor

Could return the scanned range? Then the client can keep the returned stopheight to be the next startheight.

src/wallet/rpcdump.cpp
+ if (!block) {
+ break;
+ }
+ if (!(block->pprev->nStatus & BLOCK_HAVE_DATA)) {
@promag

promag Aug 12, 2017

Contributor

block->pprev can be nullptr:

bitcoind -regtest -prune=1
bitcoin-cli -regtest rescanblockchain   <- segfault

@jonasschnelli jonasschnelli changed the title from [Wallet] Replace -rescan with a new RPC call "rescanblockchain" to [Wallet] Add RPC call "rescanblockchain <startheight> <stopheight>" Sep 6, 2017

Member

jonasschnelli commented Sep 6, 2017

Rebased and overhauled.
This no longer evicts the startup -rescan option.
Also added a RPC test.

Looks generally good. A few nits inline.

src/wallet/rpcdump.cpp
+ "rescanblockchain (\"startheight\") (\"stopheight\")\n"
+ "\nRescan the local blockchain for wallet related transactions.\n"
+ "\nArguments:\n"
+ "1. \"startheight\" (number, optional) blockheight where the rescan should start\n"
@jnewbery

jnewbery Sep 12, 2017

Member

if positive then height, if negative then depth relative to tip

In general we don't like overloading arguments with multiple meanings in this way (I know - I've tried to do it myself before and got NACKed.

The argument names should be changed to start_height and stop_height to follow style guidelines. I know that this PR was opened before the style changes so I won't suggest that you change variable names, but this is a public interface so it should adhere to the new guidelines.

src/wallet/rpcdump.cpp
+ LOCK2(cs_main, pwallet->cs_wallet);
+
+ CBlockIndex *pindexStart = nullptr;
+ CBlockIndex *pindexStop = nullptr;
@jnewbery

jnewbery Sep 12, 2017

Member

nit: slight preference to declare this variable immediately above the if (!request.params[1].isNull()) code block

@jonasschnelli

jonasschnelli Sep 16, 2017

Member

IMO they should be declared / initialised together (for better code readability).

src/wallet/rpcdump.cpp
+ if (!request.params[0].isNull()) {
+ pindexStart = chainActive[request.params[0].get_int()];
+ if (!pindexStart) {
+ throw JSONRPCError(RPC_WALLET_ERROR, "Invalid startheight");
@jnewbery

jnewbery Sep 12, 2017

Member

This should be RPC_INVALID_PARAMETER, not RPC_WALLET_ERROR

src/wallet/rpcdump.cpp
+ throw JSONRPCError(RPC_WALLET_ERROR, "Invalid startheight");
+ }
+ }
+ if (!pindexStart) {
@jnewbery

jnewbery Sep 12, 2017

Member

Why not just assign chainActive.Genesis() to pindexStart when declaring the variable (and then overwrite if !request.params[0].isNull())

@jonasschnelli

jonasschnelli Sep 16, 2017

Member

Makes sense. Will update.

src/wallet/rpcdump.cpp
+ if (!request.params[1].isNull()) {
+ pindexStop = chainActive[request.params[1].get_int()];
+ if (!pindexStop || pindexStop->nHeight < pindexStart->nHeight) {
+ throw JSONRPCError(RPC_WALLET_ERROR, "Invalid stopheight");
@jnewbery

jnewbery Sep 12, 2017

Member

As above, this should be RPC_INVALID_PARAMETER, not RPC_WALLET_ERROR

src/wallet/rpcdump.cpp
+ break;
+ }
+ if (!(block->nStatus & BLOCK_HAVE_DATA)) {
+ throw JSONRPCError(RPC_WALLET_ERROR, "Can't rescan beyond pruned data. Use RPC call getblockchaininfo to determine your pruned height.");
@jnewbery

jnewbery Sep 12, 2017

Member

This should be RPC_MISC_ERROR. Trying to scan a pruned block is not an error in the wallet.

src/wallet/rpcdump.cpp
+ }
+
+ CBlockIndex *stopBlock = nullptr;
+ if (pwallet) {
@jnewbery

jnewbery Sep 12, 2017

Member

why is this required? The call to EnsureWalletIsAvailable() above should ensure that the wallet is available.

@jonasschnelli

jonasschnelli Sep 16, 2017

Member

Right... Will remove it. I think this is a rebase issue since the PR is originally from 2015.

src/wallet/rpcdump.cpp
+ }
+
+ UniValue response(UniValue::VOBJ);
+ response.pushKV("startheight", pindexStart->nHeight);
@jnewbery

jnewbery Sep 12, 2017

Member

These return fields should be documented in the help text.

src/wallet/wallet.h
@@ -919,7 +919,7 @@ class CWallet final : public CCryptoKeyStore, public CValidationInterface
void BlockDisconnected(const std::shared_ptr<const CBlock>& pblock) override;
bool AddToWalletIfInvolvingMe(const CTransactionRef& tx, const CBlockIndex* pIndex, int posInBlock, bool fUpdate);
int64_t RescanFromTime(int64_t startTime, bool update);
- CBlockIndex* ScanForWalletTransactions(CBlockIndex* pindexStart, bool fUpdate = false);
+ CBlockIndex* ScanForWalletTransactions(CBlockIndex* pindexStart, CBlockIndex* pindexStop = nullptr, bool fUpdate = false);
@jnewbery

jnewbery Sep 12, 2017

Member

It looks like you've updated all the calls to ScanForWalletTransactions() to explicitly set pindexStop (except in wallet_tests.cpp). Why not update wallet_tests.cpp and remove the default argument. I think in general it's best to avoid default arguments if possible.

test/functional/wallet-hd.py
@@ -91,6 +91,21 @@ def run_test (self):
self.start_node(1, extra_args=self.extra_args[1] + ['-rescan'])
assert_equal(self.nodes[1].getbalance(), num_hd_adds + 1)
+ # Try a RPC based rescan
+ self.stop_node(1)
+ shutil.rmtree(tmpdir + "/node1/regtest/blocks")
@jnewbery

jnewbery Sep 12, 2017

Member

Really, this should use os.path.join to construct the path, but I see this pattern is used in other tests, so I don't think you need to change it here.

Even better would be to move these functions to be methods in the TestNode class - remove_block_files(), remove_chainstate_files(), backup_wallet_file(), restore_wallet_file(), etc.

test/functional/wallet-hd.py
+ self.start_node(1, extra_args=self.extra_args[1])
+ connect_nodes_bi(self.nodes, 0, 1)
+ self.sync_all()
+ out = self.nodes[1].rescanblockchain(0,1)
@jnewbery

jnewbery Sep 12, 2017

Member

nit: add a space: (0, 1)

test/functional/wallet-hd.py
+ assert_equal(out['stopheight'], 1)
+ out = self.nodes[1].rescanblockchain()
+ assert_equal(out['startheight'], 0)
+ assert_equal(self.nodes[1].getbalance(), num_hd_adds + 1)
@jnewbery

jnewbery Sep 12, 2017

Member

Perhaps assert that the rescan scanned up to the tip:

assert_equal(out['stopheight'], self.nodes[1].getblockcount())

src/wallet/rpcdump.cpp
+ if (!request.params[1].isNull()) {
+ pindexStop = chainActive[request.params[1].get_int()];
+ if (!pindexStop || pindexStop->nHeight < pindexStart->nHeight) {
+ throw JSONRPCError(RPC_WALLET_ERROR, "Invalid stopheight");
@luke-jr

luke-jr Sep 13, 2017

Member

If the heights are inverted, they should be flipped. 100..90 is no less rational than 90..100. This was in 79a1173, but seems to have disappeared silently in a rebase?

@jnewbery

jnewbery Sep 13, 2017

Member

@luke-jr - do you have a particular use-case for inverting the block heights? If not, I think this is fine as is. We should err towards being more strict in the interface to keep the code simpler.

@luke-jr

luke-jr Sep 13, 2017

Member

Already discussed here.

There's no reason to have an arbitrary restriction that doesn't make sense and is undocumented, especially when we can trivially just do the right thing.

@jnewbery

jnewbery Sep 13, 2017

Member

'The right thing' is a value judgement. In my opinion, the right thing is to keep APIs as tightly defined as possible in order to keep code simple and remove corner cases.

Of course, if there's a legitimate use case, we should allow both, but I don't see it.

@luke-jr

luke-jr Sep 16, 2017

Member

The legitimate use case is to rescan the blockchain... There's no reason why one direction is to be preferred over the other for these arguments.

src/wallet/rpcwallet.cpp
@@ -3235,6 +3236,7 @@ static const CRPCCommand commands[] =
{ "wallet", "removeprunedfunds", &removeprunedfunds, {"txid"} },
{ "generating", "generate", &generate, {"nblocks","maxtries"} },
+ { "wallet", "rescanblockchain", &rescanblockchain, {"startheight", "stopheight"} },
@luke-jr

luke-jr Sep 13, 2017

Member

I think this was more appropriately positioned after "removeprunedfunds" as before. Probably after "move" makes the most sense, since it seems to be alphabetised.

Member

luke-jr commented Sep 13, 2017

Also not sure rpcdump.cpp makes sense for this one.

Member

jonasschnelli commented Sep 16, 2017

Addresses @jnewbery's points.
Agree with @luke-jr that it should be in rpcwallet and not in rpcdump (moved it there).

utACK fe471a9

Did not look at test code yet.

src/wallet/rpcwallet.cpp
+ if (!request.params[0].isNull()) {
+ pindexStart = chainActive[request.params[0].get_int()];
+ if (!pindexStart) {
+ throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid startheight");
@kallewoof

kallewoof Sep 28, 2017

Member

μNit: start_height since you're calling it that in the help.

src/wallet/rpcwallet.cpp
+ if (!request.params[1].isNull()) {
+ pindexStop = chainActive[request.params[1].get_int()];
+ if (!pindexStop || pindexStop->nHeight < pindexStart->nHeight) {
+ throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid stopheight");
@kallewoof

kallewoof Sep 28, 2017

Member

μNit: stop_height

+
+ // We can't rescan beyond non-pruned blocks, stop and throw an error
+ if (fPruneMode) {
+ CBlockIndex *block = pindexStop ? pindexStop : chainActive.Tip();
@kallewoof

kallewoof Sep 28, 2017

Member

Can be abbreviated to CBlockIndex *block = pindexStop ?: chainActive.Tip();

src/wallet/rpcwallet.cpp
+ while (block) {
+ if (block->nHeight <= pindexStart->nHeight) {
+ break;
+ }
@kallewoof

kallewoof Sep 28, 2017

Member

Why not just while (block && block->nHeight > pindexStart->nHeight) {?

src/wallet/rpcwallet.cpp
+ stopBlock = pwallet->ScanForWalletTransactions(pindexStart, pindexStop, true);
+ if (!stopBlock) {
+ // if we got a nullptr returned, ScanForWalletTransactions did rescan up to the requested stopindex
+ stopBlock = pindexStop ? pindexStop : chainActive.Tip();
@kallewoof

kallewoof Sep 28, 2017

Member

Same as above here; stopBlock = pindexStop ?: chainActive.Tip();

@@ -919,7 +919,7 @@ class CWallet final : public CCryptoKeyStore, public CValidationInterface
void BlockDisconnected(const std::shared_ptr<const CBlock>& pblock) override;
bool AddToWalletIfInvolvingMe(const CTransactionRef& tx, const CBlockIndex* pIndex, int posInBlock, bool fUpdate);
int64_t RescanFromTime(int64_t startTime, bool update);
- CBlockIndex* ScanForWalletTransactions(CBlockIndex* pindexStart, bool fUpdate = false);
+ CBlockIndex* ScanForWalletTransactions(CBlockIndex* pindexStart, CBlockIndex* pindexStop, bool fUpdate = false);
@kallewoof

kallewoof Sep 28, 2017

Member

CBlockIndex* pindexStop = nullptr will let existing code default to current behavior (mostly in the test cases in wallet/test/wallet_tests.cpp).

@jonasschnelli

jonasschnelli Oct 1, 2017

Member

Heh. I had it before but then @jnewbery convinced me to remove it: #7061 (comment)

Member

jonasschnelli commented Oct 1, 2017

Adressed @kallewoof's points.

src/wallet/rpcwallet.cpp
+
+ if (request.fHelp || request.params.size() > 2) {
+ throw std::runtime_error(
+ "rescanblockchain (\"startheight\") (\"stopheight\")\n"
@kallewoof

kallewoof Oct 1, 2017

Member

I missed this last time: start_height and stop_height here to match the rest.

re-utACK e2d376b

utACK e2d376b.

Will test tomorrow. While there's few ACK's why not update variables to snake case?

src/wallet/rpcwallet.cpp
+ "2. \"stop_height\" (number, optional) block height where the rescan should stop\n"
+ "\nResult:\n"
+ "{\n"
+ " \"start_height\" : (number) The block height where the rescan has started\n"
@promag

promag Oct 1, 2017

Contributor

Nit, remove space before :.

2 nits and a minor bug

src/wallet/rpcwallet.cpp
+ "rescanblockchain (\"start_height\") (\"stop_height\")\n"
+ "\nRescan the local blockchain for wallet related transactions.\n"
+ "\nArguments:\n"
+ "1. \"start_height\" (number, optional) block height where the rescan should start\n"
@jnewbery

jnewbery Oct 2, 2017

Member

nit: add "If omitted, rescan starts from the genesis block."

@MeshCollider

MeshCollider Oct 5, 2017

Member

Or just default=0

src/wallet/rpcwallet.cpp
+ "\nRescan the local blockchain for wallet related transactions.\n"
+ "\nArguments:\n"
+ "1. \"start_height\" (number, optional) block height where the rescan should start\n"
+ "2. \"stop_height\" (number, optional) block height where the rescan should stop\n"
@jnewbery

jnewbery Oct 2, 2017

Member

nit: add "If omitted, rescan stops at the chain tip."

@JeremyRubin

JeremyRubin Oct 4, 2017

Contributor

Please specify if the scan stops before or after this block.

I think semantically it makes more sense to provide an inclusive range (if I say "scan blocks 10-20", not scanning 20 seems confusing).

If you really want an exclusive range, providing the number of blocks to scan (e.g., "scan 10 blocks starting at 10") has better ux, but still worse than inclusive.

src/wallet/rpcwallet.cpp
+ CBlockIndex *stopBlock = nullptr;
+ stopBlock = pwallet->ScanForWalletTransactions(pindexStart, pindexStop, true);
+ if (!stopBlock) {
+ // if we got a nullptr returned, ScanForWalletTransactions did rescan up to the requested stopindex
@jnewbery

jnewbery Oct 2, 2017

Member

minor bug: sadly, the comment about the return value for ScanForWalletTransactions() added in #10208 is incomplete. If abortrescan is called during a rescan, then ScanForWalletTransactions() will return nullptr even if the though scan didn't complete successfully to the tip (or requested pindexStop after this PR)

I think the only way to correctly return the status of the rescan would be to have more information passed back by ScanForWalletTransactions().

(Test this by running rescanblockchain in one window and calling abortrescan in a different window. rescanblockchain returns the requested stop_height even though the rescan didn't extend that far).

@ryanofsky since he changed the semantics of ScanForWalletTransactions() in #10208.

@jonasschnelli

jonasschnelli Oct 4, 2017

Member

Nice catch... added a protection that detects aborted rescans via a check of CWallet::IsAbortingRescan() after ScanForWalletTransactions().

@jnewbery

jnewbery Oct 4, 2017

Member

Seems like a good pragmatic change. There's a (pathological) race condition where another thread starts a rescan and resets fAbortRescan before this RPC returns, but that's extremely unlikely.

I still think it would be nice to have ScanForWalletTransactions() give a more meaningful return value that indicates whether the rescan was aborted. The same bug exists in importmulti where RescanFromTime() will return the wrong time value if the rescan is aborted. However, that fix can be for another PR.

Member

jonasschnelli commented Oct 4, 2017

Fixed @jnewbery and @promag's nits.

@@ -10,6 +10,7 @@
connect_nodes_bi,
)
import shutil
+import os
@jnewbery

jnewbery Oct 4, 2017

Member

nit: please sort imports :)

and ideally place them in PEP8 ordering (ie standard library before project imports)

Member

jnewbery commented Oct 4, 2017

Looks good. One style nit, but ACK bdae58e with or without.

src/wallet/rpcwallet.cpp
+ }
+ }
+
+ CBlockIndex *stopBlock = nullptr;
@promag

promag Oct 4, 2017

Contributor

Join this with next line.

+ if (pwallet->IsAbortingRescan()) {
+ throw JSONRPCError(RPC_MISC_ERROR, "Rescan aborted.");
+ }
+ // if we got a nullptr returned, ScanForWalletTransactions did rescan up to the requested stopindex
@promag

promag Oct 4, 2017

Contributor

This is wrong, from ScanForWalletTransactions doc:

Returns null if scan was successful. Otherwise, if a complete rescan was not possible (due to pruning or corruption), returns pointer to the most recent block that could not be scanned.

So it can return a non nullptr but catch transactions further in the chain. It only stops rescanning when abortrescan RPC is called.

@promag

promag Oct 4, 2017

Contributor

Maybe we should add the most recent block that could not be scanned to the response.

@jonasschnelli

jonasschnelli Oct 4, 2017

Member

It's not correct, right. I think we leave that fix for another PR (not directly related to this PR).

src/wallet/rpcwallet.cpp
+ throw JSONRPCError(RPC_MISC_ERROR, "Rescan aborted.");
+ }
+ // if we got a nullptr returned, ScanForWalletTransactions did rescan up to the requested stopindex
+ stopBlock = pindexStop ?: chainActive.Tip();
@promag

promag Oct 4, 2017

Contributor

Avoid this GNU extension (empty operand)?

@promag

promag Oct 4, 2017

Contributor

BTW, first time in this source code.

@kallewoof

kallewoof Oct 4, 2017

Member

I did not know a ?: b was a GNU extension. Sorry for bad nit earlier. :(

Concept Ack.

However, the handling of the ranges is incorrect afaict and needs to be corrected (and better documented).

src/wallet/rpcwallet.cpp
+ "\nRescan the local blockchain for wallet related transactions.\n"
+ "\nArguments:\n"
+ "1. \"start_height\" (number, optional) block height where the rescan should start\n"
+ "2. \"stop_height\" (number, optional) block height where the rescan should stop\n"
@JeremyRubin

JeremyRubin Oct 4, 2017

Contributor

Please specify if the scan stops before or after this block.

I think semantically it makes more sense to provide an inclusive range (if I say "scan blocks 10-20", not scanning 20 seems confusing).

If you really want an exclusive range, providing the number of blocks to scan (e.g., "scan 10 blocks starting at 10") has better ux, but still worse than inclusive.

src/wallet/rpcwallet.cpp
+ "2. \"stop_height\" (number, optional) block height where the rescan should stop\n"
+ "\nResult:\n"
+ "{\n"
+ " \"start_height\" (number) The block height where the rescan has started. If omitted, rescan starts from the genesis block.\n"
@JeremyRubin

JeremyRubin Oct 4, 2017

Contributor

tense error s/start/started

src/wallet/rpcwallet.cpp
+ "\nResult:\n"
+ "{\n"
+ " \"start_height\" (number) The block height where the rescan has started. If omitted, rescan starts from the genesis block.\n"
+ " \"stop_height\" (number) The height of the last rescanned block. If omitted, rescan stops at the chain tip.\n"
@JeremyRubin

JeremyRubin Oct 4, 2017

Contributor

tense error s/stops/stopped

@JeremyRubin

JeremyRubin Oct 4, 2017

Contributor

This is not accurate AFAICT. The block at stop_height is not scanned?

@jonasschnelli

jonasschnelli Oct 4, 2017

Member

AFAIK the block defined with stop_height will be scanned.

src/wallet/rpcwallet.cpp
+
+ if (!request.params[1].isNull()) {
+ pindexStop = chainActive[request.params[1].get_int()];
+ if (!pindexStop || pindexStop->nHeight < pindexStart->nHeight) {
@JeremyRubin

JeremyRubin Oct 4, 2017

Contributor

I think with this (and with the above pindexStart check), I'd like to see these errors differentiate between:

  1. Invalid Start Height: no negative heights
  2. Invalid Start Height: Beyond what's been synced
  3. Invalid Stop Height: No negative heights
  4. Invalid Stop Height: Beyond what's been synced
  5. Invalid Range: stop must be greater than start.

At the very least, 5. should be represented because it could be the start_height which is invalid.

@jonasschnelli

jonasschnelli Oct 4, 2017

Member

Added case 5.

src/wallet/rpcwallet.cpp
+ CBlockIndex *block = pindexStop ?: chainActive.Tip();
+ while (block && block->nHeight > pindexStart->nHeight) {
+ if (!(block->nStatus & BLOCK_HAVE_DATA)) {
+ throw JSONRPCError(RPC_MISC_ERROR, "Can't rescan beyond pruned data. Use RPC call getblockchaininfo to determine your pruned height.");
@JeremyRubin

JeremyRubin Oct 4, 2017

Contributor

There is a race condition for using getblockchaininfo RPC call if a new block comes in, unless you can pause pruning.

Maybe worth just documenting to retry until there is a pruning pause feature.

src/wallet/rpcwallet.cpp
+ }
+
+ UniValue response(UniValue::VOBJ);
+ response.pushKV("start_height", pindexStart->nHeight);
@JeremyRubin

JeremyRubin Oct 4, 2017

Contributor

If no blocks are re scanned, I don't think this is correct/is confusing.

+
+ UniValue response(UniValue::VOBJ);
+ response.pushKV("start_height", pindexStart->nHeight);
+ response.pushKV("stop_height", stopBlock->nHeight);
@JeremyRubin

JeremyRubin Oct 4, 2017

Contributor

According to the ScanForWalletTransactions docs...

* Returns null if scan was successful. Otherwise, if a complete rescan was not
* possible (due to pruning or corruption), returns pointer to the most recent
* block that could not be scanned.

So with the current returned value it should be nHeight-1.

Member

jonasschnelli commented Oct 4, 2017

@JeremyRubin the current implementation does scan the block defined with the stop_height parameter. I'll update the parameter documentation to make this more clear.

Member

jonasschnelli commented Oct 4, 2017

I think the range handling is correct but change the RPC help to \"stop_height\" (number, optional) the last block height that should be scanned.

Reverted the conditional ?: op abbreviation due to the fact that this requires a GNU extension.

The fix for the misleading return value of ScanForWalletTransaction and comment (#11450) should be handled outside of this PR.

Ok. Let's say that we say run

 rescanblockchain { start_height: 10, stop_height: 20 }

then in ScanForWalletTransactions, we fail at 20.

We will return

{ start_height: 10, stop_height: 20 }

which isn't true, because we failed to scan 20.

This is also true if we fail, at say, 15.

We will return

{ start_height: 10, stop_height: 15 }

Which is also not true because we didn't scan 15.

src/wallet/wallet.cpp
@@ -1555,12 +1555,19 @@ int64_t CWallet::RescanFromTime(int64_t startTime, bool update)
* Returns null if scan was successful. Otherwise, if a complete rescan was not
* possible (due to pruning or corruption), returns pointer to the most recent
* block that could not be scanned.
+ *
+ * If pindexStop is not a nullptr, the scan will stop one block after the block-index
@JeremyRubin

JeremyRubin Oct 4, 2017

Contributor

I think this is clearest to say:

If pindexStop is not a nullptr, we attempt to scan up to and including pIndexStop.
Contributor

JeremyRubin commented Oct 4, 2017

Also, more generally, it seems we could fail to scan many blocks in the range and still return that we scanned the range.

Member

jonasschnelli commented Oct 4, 2017

@JeremyRubin: Why do you think rescanblockchain { start_height: 10, stop_height: 20 } would not scan block at height 20? I just re-checked, re-tested and it looks like it does scan block 20.

Member

jonasschnelli commented Oct 4, 2017

I understand @JeremyRubin concern now. It's about corrupted blocks (when ReadBlockFromDisk fails). The current startup -rescans do sort of tolerate this.

Added a fix that now leads to an error thrown when detecting a corrupted block (https://github.com/bitcoin/bitcoin/pull/7061/files#diff-df7d84ff2f53fcb2a0dc15a3a51e55ceR3237)

Member

jtimon commented Oct 5, 2017

Concept ACK.
This seem like moving in the right direction, even if in the long term we want to avoid the need for rescans completely.

This now does replace the -rescan startup argument with a new RPC call rescanblockchain.

I don't see this in the code. Shouldn't we at least deprecate the startup argument at the same time? (I would not oppose to directly remove it as an exception to the general policy instead of waiting for 0.17).
Perhaps note in the release notes that this is also supposed to be temporary.

Member

jonasschnelli commented Oct 5, 2017

This now does replace the -rescan startup argument with a new RPC call rescanblockchain.

I don't see this in the code. Shouldn't we at least deprecate the startup argument at the same time? (I would not oppose to directly remove it as an exception to the general policy instead of waiting for 0.17).
Perhaps note in the release notes that this is also supposed to be temporary.

This was removed from the PRs description (but not from a later comment, now added a strike-through attr.)

Contributor

promag commented Oct 5, 2017

IMO this is ready to merge even though there are some concerns that need to be addressed in follow ups:

  • Rescan continues even if a corrupted block is detected but the RPC fails to the caller;
  • rescanblockchain can be refactored a little to avoid the cs_main and cs_wallet locks;

I also would like to discuss the option to make this RPC asynchronous so the caller doesn't wait for the rescan to complete, it only asks for a rescan. There is a big chance the caller interrupts the call, but I believe in server side the rescan continues.

utACK 559542a.

src/wallet/rpcwallet.cpp
+ stopBlock = pindexStop ? pindexStop : chainActive.Tip();
+ }
+ else {
+ throw JSONRPCError(RPC_MISC_ERROR, "Rescan failed. Potentially corruputed data files.");
@MeshCollider

MeshCollider Oct 5, 2017

Member

Typo corruputed -> corrupted

src/wallet/rpcwallet.cpp
+ " \"stop_height\" (number) The height of the last rescanned block. If omitted, rescan stopped at the chain tip.\n"
+ "}\n"
+ "\nExamples:\n"
+ + HelpExampleCli("rescanblockchain", "\"100000 120000\"")
@MeshCollider

MeshCollider Oct 5, 2017

Member

These quotes look wrong, should both start and stop height be surrounded in the same string? As numbers should they even have quotes around them?
HelpExampleRpc should also have commas between the arguments I believe
Small nit, number -> numeric in the lines above to be consistent with other calls

Member

MeshCollider commented Oct 5, 2017

re-utACK 559542a modulo comments above

Member

jonasschnelli commented Oct 9, 2017

Fixed @MeshCollider nits.

Contributor

JeremyRubin commented Oct 9, 2017

I still think it's worth it to handle

  1. Invalid Start Height: no negative heights
  2. Invalid Stop Height: No negative heights

and

  1. Invalid Start Height: Beyond what's been synced
  2. Invalid Stop Height: Beyond what's been synced

differently. Specifically, the latter calls could still be handled and processed.

Member

jnewbery commented Oct 10, 2017

Tested ACK 35e7fd1

I still think it's worth it to handle ... differently.

These already fail with Invalid start_height and Invalid stop_height. Yes, we can always provide more detailed error messages or logging, but lets not hold this PR up on that. It's already been very heavily reviewed.

I'll happily review follow-up PRs if you want to change error logging.

+ "}\n"
+ "\nExamples:\n"
+ + HelpExampleCli("rescanblockchain", "100000 120000")
+ + HelpExampleRpc("rescanblockchain", "100000 120000")
@MeshCollider

MeshCollider Oct 10, 2017

Member

I think the HelpExampleRpc should have a comma between the arguments, i.e. a comma after 100000
Yeah would be good to get this merged, sorry for yet another nit :)

@@ -3233,6 +3309,7 @@ static const CRPCCommand commands[] =
{ "wallet", "walletpassphrasechange", &walletpassphrasechange, {"oldpassphrase","newpassphrase"} },
{ "wallet", "walletpassphrase", &walletpassphrase, {"passphrase","timeout"} },
{ "wallet", "removeprunedfunds", &removeprunedfunds, {"txid"} },
+ { "wallet", "rescanblockchain", &rescanblockchain, {"start_height", "stop_height"} },
@kallewoof

kallewoof Oct 11, 2017

Member

Very tiny nit, but every other place skips the space between words in argument list. I.e. {"start_height","stop_height"}.

re-utACK 35e7fd1

Contributor

JeremyRubin commented Oct 12, 2017

@jnewbery to be clear

  1. I don't think it's fair to say it was heavily reviewed and I'm needlessly holding it up, I found a major bug in the implementation which required a (imo) pretty significant change to the semantics of the return value.

  2. I'm not suggesting a change in error reporting, I am suggesting a functional change to the ranges which are handled by this call. Specifically, I would like for the cases where

    1. Invalid Start Height: Beyond what's been synced
    2. Invalid Stop Height: Beyond what's been synced

to not throw an error and return a successful scan range (if possible).

Member

jnewbery commented Oct 12, 2017

@JeremyRubin - sorry if that came off as a personal criticism. That's not what I meant. This PR was re-opened in December last year and has been reviewed by 9 people so far. It's very useful functionality and it'd be great to see it merged. And yes - you did catch a subtle bug in your review which the rest of us missed. Thank you!

re: your suggested change to the interface - if start_height is beyond what's been sync'ed, then there's nothing to rescan and we should return an error to make it clear to the user that the call was a no-op. If stop_height is beyond the sync height, then it's safer to return an error and let the user call the method again with a valid stop_height. If the call succeeded then a user who's not paying close attention to the return value may incorrectly assume that the wallet is rescanned up to the requested stop_height.

I haven't followed most of the review conversation, but would give light conditional utACK for 35e7fd1 if >= comment below is addressed.

I like @JeremyRubin's suggestion of avoiding errors when rescans are requested beyond the synced range, but it seems like that could easily be added in a followup.

src/wallet/rpcwallet.cpp
+ // We can't rescan beyond non-pruned blocks, stop and throw an error
+ if (fPruneMode) {
+ CBlockIndex *block = pindexStop ? pindexStop : chainActive.Tip();
+ while (block && block->nHeight > pindexStart->nHeight) {
@ryanofsky

ryanofsky Oct 12, 2017

Contributor

Should this be >=? Otherwise it won't check the start block. Maybe add comment if there's a special reason for skipping the start block.

jonasschnelli added some commits Nov 19, 2015

Member

jonasschnelli commented Oct 12, 2017

Fixed @ryanofsky points with the >= check.
Lets merge this now,... I think the ranges cleanup (if we want to do this) could be PRed by @JeremyRubin after this PR.

Contributor

ryanofsky commented Oct 12, 2017

utACK 7a91ceb. Only change since last review was >= fix

Member

jnewbery commented Oct 12, 2017

reACK 7a91ceb

@jonasschnelli jonasschnelli merged commit 7a91ceb into bitcoin:master Oct 13, 2017

1 check passed

continuous-integration/travis-ci/pr The Travis CI build passed
Details

jonasschnelli added a commit that referenced this pull request Oct 13, 2017

Merge #7061: [Wallet] Add RPC call "rescanblockchain <startheight> <s…
…topheight>"


7a91ceb [QA] Add RPC based rescan test (Jonas Schnelli)
c77170f [Wallet] add rescanblockchain <start_height> <stop_height> RPC command (Jonas Schnelli)

Pull request description:

  A RPC rescan command is much more flexible for the following reasons:
  * You can define the start and end-height
  * It can be called during runtime
  * It can work in multiwallet environment

Tree-SHA512: df67177bad6ad1d08e5a621f095564524fa3eb87204c2048ef7265e77013e4b1b29f991708f807002329a507a254f35e79a4ed28a2d18d4b3da7a75d57ce0ea5
Member

jnewbery commented Oct 13, 2017

🎉

jonasschnelli added a commit that referenced this pull request Oct 16, 2017

Merge #11496: [Trivial] Add missing comma from rescanblockchain example
43f76f6 Add missing comma from rescanblockchain (MeshCollider)

Pull request description:

  #7061 forgot a comma in the HelpExampleRpc() for the rescanblockchain RPC, giving an incorrect example command output:
  > curl --user myusername --data-binary '{"jsonrpc": "1.0", "id":"curltest", "method": "rescanblockchain", "params": [100000 120000] }' -H 'content-type: text/plain;' http://127.0.0.1:8332/

  Was just missed during nit-fixing. This is a trivial fix to add that comma in.

Tree-SHA512: b808f32674af585a1ddb78b25621dff0387dbad79c97d65ff61d8a9a12a94e4b8ecf03eda3f281fe439bddb6c0703c39104dbb279f1718949abd930faaa9042f

luke-jr added a commit to bitcoinknots/bitcoin that referenced this pull request Nov 6, 2017

luke-jr added a commit to bitcoinknots/bitcoin that referenced this pull request Nov 6, 2017

luke-jr added a commit to bitcoinknots/bitcoin that referenced this pull request Nov 6, 2017

[QA] Add RPC based rescan test
Github-Pull: #7061
Rebased-From: bea58bcc248fed84627142447b74d78e299c04e7
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment