Join GitHub today
GitHub is home to over 20 million developers working together to host and review code, manage projects, and build software together.
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
Conversation
jonasschnelli
added
Wallet
RPC/REST/ZMQ
labels
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? |
|
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 |
|
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. |
|
@lannwj I thought thats part of what the height parameter here was for-- addressing pruning comparability? |
|
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? |
|
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. |
|
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. |
|
@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") |
|
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
and 1 other
commented on an outdated diff
Nov 23, 2015
| + + 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) |
|
|
|
agree with gmaxwell that this should scan a range of blocks |
|
Agree with the stop parameter. Working on a implementation.... |
|
I would still prefer an approach that imports with birthdate instead of explicit rescanning. |
|
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 |
|
@jonasschnelli see #6570 (comment) regarding your last comment. |
GIJensen
commented
Dec 7, 2015
|
ACK |
|
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. |
MarcoFalke
referenced this pull request
May 1, 2016
Closed
Optional parameter for rescans to start at a specified height #7984
promag
commented on an outdated diff
May 2, 2016
laanwj
added
the
Feature
label
Jun 16, 2016
added a commit
to bitcoinknots/bitcoin
that referenced
this pull request
Jun 28, 2016
|
Rebased. |
|
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. |
|
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 |
|
I think we should work on |
Yes. I can agree with that. |
luke-jr
referenced this pull request
Sep 6, 2016
Open
Feature Request: -rescan pruned blockchain #8668
|
Another thing where this could be useful is restoring hd wallets |
|
Since importmulti #7551 is merged, some use cases are covered by that. |
Tend to agree. Closing this for now. |
laanwj
closed this
Nov 2, 2016
|
IMO something like that would be very handy if you rescan a HD wallet (old backup). |
jonasschnelli
reopened this
May 24, 2017
jonasschnelli
changed the title from
[Wallet] add rescanblockchain <height> RPC command
to
[Wallet] Replace -rescan with a new RPC call "rescanblockchain"
May 24, 2017
|
Reopened and overhauled.
Using |
ryanofsky
reviewed
May 24, 2017
Should add some test coverage for this. One easy way might be to add a new Rescan.manual enum value here:
bitcoin/test/functional/import-rescan.py
Line 32 in 4677151
if self.rescan == Rescan.manual:
self.node.rescanblockchain()
at the end of the do_import method.
| @@ -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
May 24, 2017
Contributor
I think these files are automatically generated from -help output and could be reverted in the PR.
| @@ -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
May 24, 2017
Contributor
Maybe still worth mentioning that pruning can get in the way of successfully importing wallet keys & rescanning.
| + "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
May 24, 2017
Contributor
Maybe this should take either times or heights like the pruneblockchain RPC:
bitcoin/src/rpc/blockchain.cpp
Line 838 in 4677151
jnewbery
May 24, 2017
Member
suggest you change the argument names to startheight and stopheight. No other rpcs use - as word delimiter.
| + pIndexStart = chainActive.Genesis(); | ||
| + | ||
| + //We can't rescan beyond non-pruned blocks, stop and throw an error | ||
| + if (fPruneMode) |
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)
| + block = block->pprev; | ||
| + | ||
| + if (pIndexStart->nHeight < block->nHeight) | ||
| + throw JSONRPCError(RPC_WALLET_ERROR, "Can't rescan beyond pruned data."); |
ryanofsky
May 24, 2017
Contributor
Might be helpful if error message could mention getblockchaininfo RPC for getting pruned height.
| @@ -1508,6 +1512,9 @@ CBlockIndex* CWallet::ScanForWalletTransactions(CBlockIndex* pindexStart, bool f | ||
| } else { | ||
| ret = nullptr; | ||
| } | ||
| + if (pindex == pindexStop) { |
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.
| @@ -3865,7 +3871,7 @@ CWallet* CWallet::CreateWalletFromFile(const std::string walletFile) | ||
| RegisterValidationInterface(walletInstance); | ||
| CBlockIndex *pindexRescan = chainActive.Genesis(); | ||
| - if (!GetBoolArg("-rescan", false)) |
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
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
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
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
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
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.
| @@ -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
May 24, 2017
Member
I can't see where rescanblockchain() is being called. Am I missing something?
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
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
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
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
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
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.
| + "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
May 24, 2017
Member
suggest you change the argument names to startheight and stopheight. No other rpcs use - as word delimiter.
| + | ||
| + CBlockIndex *pIndexStart = NULL; | ||
| + CBlockIndex *pIndexStop = NULL; | ||
| + if (request.params.size() > 0 && request.params[0].isNum()) |
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()) {
| + 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
May 24, 2017
Member
should this return an error if heightstop is lower than heightstart? If heightstop is higher than the tip height?
| + if (pwallet) | ||
| + pwallet->ScanForWalletTransactions(pIndexStart, pIndexStop, true); | ||
| + | ||
| + return NullUniValue; |
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
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).
| @@ -1468,11 +1468,15 @@ void CWalletTx::GetAmounts(std::list<COutputEntry>& listReceived, | ||
| * before CWallet::nTimeFirstKey). Returns null if there is no such range, or |
jnewbery
May 24, 2017
Member
nit: update function comment to say that the rescan is up to pindexStop if it is non-null
| @@ -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
May 24, 2017
Member
Need to remove more of this comment, so that it just reads:
// show rescan progress in GUI as dialog
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?
| @@ -3865,7 +3871,7 @@ CWallet* CWallet::CreateWalletFromFile(const std::string walletFile) | ||
| RegisterValidationInterface(walletInstance); | ||
| CBlockIndex *pindexRescan = chainActive.Genesis(); | ||
| - if (!GetBoolArg("-rescan", false)) |
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.
added a commit
to bitcoinknots/bitcoin
that referenced
this pull request
Jun 15, 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. |
|
Rebased. |
ryanofsky
reviewed
Jul 25, 2017
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.
| + "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" |
| + "\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
Jul 25, 2017
Contributor
Could do what pruneblockchain does and allow height to be specified either as a physical height or a time:
bitcoin/src/rpc/blockchain.cpp
Line 855 in 1caafa6
| + | ||
| + CBlockIndex *pIndexStart = NULL; | ||
| + CBlockIndex *pIndexStop = NULL; | ||
| + if (request.params.size() > 0 && request.params[0].isNum()) { |
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.
| + //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
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).
| + if (pwallet) | ||
| + pwallet->ScanForWalletTransactions(pIndexStart, pIndexStop, true); | ||
| + | ||
| + return NullUniValue; |
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).
|
If #10941 goes first then the test can be extended to do a |
|
Rebased and addressed @ryanofsky 's comments on my |
|
Cherry picked @luke-jr rebased / overhauled version. |
promag
reviewed
Aug 12, 2017
Need to change the progress calculation:
double dProgressTip = GuessVerificationProgress(chainParams.TxData(), chainActive.Tip());Should rename to dProgressStop.
| + | ||
| + if (request.fHelp || request.params.size() > 2) { | ||
| + throw std::runtime_error( | ||
| + "rescanblockchain (\"startheight\") (\"stopheight\")\n" |
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.
| + "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
Aug 12, 2017
Contributor
Just start?
Also, if positive then height, if negative then depth relative to tip. Same for stop.
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.
| + "\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" |
| + | ||
| + LOCK2(cs_main, pwallet->cs_wallet); | ||
| + | ||
| + CBlockIndex *pindexStart = NULL; |
| + | ||
| + CBlockIndex *pindexStart = NULL; | ||
| + CBlockIndex *pindexStop = NULL; | ||
| + if (request.params[0].isNum()) { |
promag
Aug 12, 2017
Contributor
IMO if (!request.params[0].isNull()) so that get_int throws if invalid value is supplied.
| + pindexStart = chainActive.Genesis(); | ||
| + } | ||
| + | ||
| + if (request.params[1].isNum()) { |
| + | ||
| + if (request.params[1].isNum()) { | ||
| + pindexStop = chainActive[request.params[1].get_int()]; | ||
| + } |
| + } | ||
| + | ||
| + if (pindexStop && pindexStop->nHeight < pindexStart->nHeight) { | ||
| + // Flip the parameters to the expected order |
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.
| + | ||
| + if (pindexStop && pindexStop->nHeight < pindexStart->nHeight) { | ||
| + // Flip the parameters to the expected order | ||
| + CBlockIndex * const tmp = pindexStart; |
| + // 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
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
Aug 12, 2017
Member
I think this may be currently true, but perhaps not safe to assume for the future. Unsure.
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
Sep 5, 2017
Member
We loop backwards here which the check is necessary. Or am I wrong?
promag
Sep 6, 2017
Contributor
Something is wrong now because nothing changes the the loop exit conditions.
| { | ||
| int64_t nNow = GetTime(); | ||
| const CChainParams& chainParams = Params(); | ||
| + if (pindexStop && pindexStop->nHeight < pindexStart->nHeight) { |
jonasschnelli
Oct 4, 2017
Member
Can happen from the perspective of the function. But maybe not with the current consumption of the function.
| + pwallet->ScanForWalletTransactions(pindexStart, pindexStop, true); | ||
| + } | ||
| + | ||
| + return NullUniValue; |
promag
Aug 12, 2017
Contributor
Could return the scanned range? Then the client can keep the returned stopheight to be the next startheight.
| + if (!block) { | ||
| + break; | ||
| + } | ||
| + if (!(block->pprev->nStatus & BLOCK_HAVE_DATA)) { |
promag
Aug 12, 2017
Contributor
block->pprev can be nullptr:
bitcoind -regtest -prune=1
bitcoin-cli -regtest rescanblockchain <- segfault
promag
referenced this pull request
Aug 24, 2017
Open
`addwitnessaddress` should have a `rescan` option similar to `importprivkey` #10016
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
|
Rebased and overhauled. |
| + "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
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.
| + LOCK2(cs_main, pwallet->cs_wallet); | ||
| + | ||
| + CBlockIndex *pindexStart = nullptr; | ||
| + CBlockIndex *pindexStop = nullptr; |
jnewbery
Sep 12, 2017
Member
nit: slight preference to declare this variable immediately above the if (!request.params[1].isNull()) code block
jonasschnelli
Sep 16, 2017
Member
IMO they should be declared / initialised together (for better code readability).
| + if (!request.params[0].isNull()) { | ||
| + pindexStart = chainActive[request.params[0].get_int()]; | ||
| + if (!pindexStart) { | ||
| + throw JSONRPCError(RPC_WALLET_ERROR, "Invalid startheight"); |
| + throw JSONRPCError(RPC_WALLET_ERROR, "Invalid startheight"); | ||
| + } | ||
| + } | ||
| + if (!pindexStart) { |
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())
| + 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"); |
| + 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
Sep 12, 2017
Member
This should be RPC_MISC_ERROR. Trying to scan a pruned block is not an error in the wallet.
| + } | ||
| + | ||
| + CBlockIndex *stopBlock = nullptr; | ||
| + if (pwallet) { |
jnewbery
Sep 12, 2017
Member
why is this required? The call to EnsureWalletIsAvailable() above should ensure that the wallet is available.
jonasschnelli
Sep 16, 2017
Member
Right... Will remove it. I think this is a rebase issue since the PR is originally from 2015.
| + } | ||
| + | ||
| + UniValue response(UniValue::VOBJ); | ||
| + response.pushKV("startheight", pindexStart->nHeight); |
| @@ -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
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.
| @@ -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
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.
| + 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) |
| + 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
Sep 12, 2017
Member
Perhaps assert that the rescan scanned up to the tip:
assert_equal(out['stopheight'], self.nodes[1].getblockcount())
| + 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
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
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
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
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
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.
| @@ -3235,6 +3236,7 @@ static const CRPCCommand commands[] = | ||
| { "wallet", "removeprunedfunds", &removeprunedfunds, {"txid"} }, | ||
| { "generating", "generate", &generate, {"nblocks","maxtries"} }, | ||
| + { "wallet", "rescanblockchain", &rescanblockchain, {"startheight", "stopheight"} }, |
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.
|
Also not sure |
kallewoof
approved these changes
Sep 28, 2017
utACK fe471a9
Did not look at test code yet.
| + if (!request.params[0].isNull()) { | ||
| + pindexStart = chainActive[request.params[0].get_int()]; | ||
| + if (!pindexStart) { | ||
| + throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid startheight"); |
| + 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"); |
| + | ||
| + // We can't rescan beyond non-pruned blocks, stop and throw an error | ||
| + if (fPruneMode) { | ||
| + CBlockIndex *block = pindexStop ? pindexStop : chainActive.Tip(); |
kallewoof
Sep 28, 2017
Member
Can be abbreviated to CBlockIndex *block = pindexStop ?: chainActive.Tip();
| + while (block) { | ||
| + if (block->nHeight <= pindexStart->nHeight) { | ||
| + break; | ||
| + } |
kallewoof
Sep 28, 2017
Member
Why not just while (block && block->nHeight > pindexStart->nHeight) {?
| + 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(); |
| @@ -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
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
Oct 1, 2017
Member
Heh. I had it before but then @jnewbery convinced me to remove it: #7061 (comment)
|
Adressed @kallewoof's points. |
| + | ||
| + if (request.fHelp || request.params.size() > 2) { | ||
| + throw std::runtime_error( | ||
| + "rescanblockchain (\"startheight\") (\"stopheight\")\n" |
kallewoof
Oct 1, 2017
Member
I missed this last time: start_height and stop_height here to match the rest.
promag
reviewed
Oct 1, 2017
utACK e2d376b.
Will test tomorrow. While there's few ACK's why not update variables to snake case?
| + "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" |
| + "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" |
| + "\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
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.
| + 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
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
Oct 4, 2017
Member
Nice catch... added a protection that detects aborted rescans via a check of CWallet::IsAbortingRescan() after ScanForWalletTransactions().
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.
| @@ -10,6 +10,7 @@ | ||
| connect_nodes_bi, | ||
| ) | ||
| import shutil | ||
| +import os |
jnewbery
Oct 4, 2017
Member
nit: please sort imports :)
and ideally place them in PEP8 ordering (ie standard library before project imports)
|
Looks good. One style nit, but ACK bdae58e with or without. |
| + } | ||
| + } | ||
| + | ||
| + CBlockIndex *stopBlock = nullptr; |
| + 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
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
Oct 4, 2017
Contributor
Maybe we should add the most recent block that could not be scanned to the response.
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).
| + 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(); |
kallewoof
Oct 4, 2017
Member
I did not know a ?: b was a GNU extension. Sorry for bad nit earlier. :(
JeremyRubin
suggested changes
Oct 4, 2017
Concept Ack.
However, the handling of the ranges is incorrect afaict and needs to be corrected (and better documented).
| + "\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
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.
| + "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" |
| + "\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
Oct 4, 2017
Contributor
This is not accurate AFAICT. The block at stop_height is not scanned?
| + | ||
| + if (!request.params[1].isNull()) { | ||
| + pindexStop = chainActive[request.params[1].get_int()]; | ||
| + if (!pindexStop || pindexStop->nHeight < pindexStart->nHeight) { |
JeremyRubin
Oct 4, 2017
Contributor
I think with this (and with the above pindexStart check), I'd like to see these errors differentiate between:
- Invalid Start Height: no negative heights
- Invalid Start Height: Beyond what's been synced
- Invalid Stop Height: No negative heights
- Invalid Stop Height: Beyond what's been synced
- 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.
| + 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
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.
| + } | ||
| + | ||
| + UniValue response(UniValue::VOBJ); | ||
| + response.pushKV("start_height", pindexStart->nHeight); |
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
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.
|
@JeremyRubin the current implementation does scan the block defined with the |
|
I think the range handling is correct but change the RPC help to 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. |
JeremyRubin
suggested changes
Oct 4, 2017
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.
| @@ -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
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.
|
Also, more generally, it seems we could fail to scan many blocks in the range and still return that we scanned the range. |
|
@JeremyRubin: Why do you think |
|
I understand @JeremyRubin concern now. It's about corrupted blocks (when 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) |
|
Concept ACK.
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). |
This was removed from the PRs description (but not from a later comment, now added a strike-through attr.) |
|
IMO this is ready to merge even though there are some concerns that need to be addressed in follow ups:
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. |
| + stopBlock = pindexStop ? pindexStop : chainActive.Tip(); | ||
| + } | ||
| + else { | ||
| + throw JSONRPCError(RPC_MISC_ERROR, "Rescan failed. Potentially corruputed data files."); |
| + " \"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
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
|
re-utACK 559542a modulo comments above |
jonasschnelli
added
the
Needs release notes
label
Oct 9, 2017
|
Fixed @MeshCollider nits. |
|
I still think it's worth it to handle
and
differently. Specifically, the latter calls could still be handled and processed. |
|
Tested ACK 35e7fd1
These already fail with 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
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
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"}.
|
@jnewbery to be clear
to not throw an error and return a successful scan range (if possible). |
|
@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 |
ryanofsky
reviewed
Oct 12, 2017
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.
| + // 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
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
|
Fixed @ryanofsky points with the >= check. |
|
utACK 7a91ceb. Only change since last review was >= fix |
|
reACK 7a91ceb |
jonasschnelli
merged commit 7a91ceb
into
bitcoin:master
Oct 13, 2017
1 check passed
added a commit
that referenced
this pull request
Oct 13, 2017
|
|
jonasschnelli commentedNov 19, 2015
•
Edited 1 time
-
jonasschnelli
May 24, 2017
A RPC rescan command is much more flexible for the following reasons: