Skip to content

Conversation

jnewbery
Copy link
Contributor

@jnewbery jnewbery commented Jul 19, 2017

  • Add keypool_critical (configurable). If the number of keys in the keypool drops below this limit while the wallet is rescanning, shutdown the node. Do not shutdown the node if the wallet is 'current', ie it is receiving BlockConnected calls from the node.
  • Add keypool_min (configurable). If the number of keys in the keypool drops below this limit, stop advancing the wallet's best block. This forces the wallet to rescan from the point that it dropped below the limit the next time that it starts up. This is a toggle controlled by m_update_best_block, which doesn't get unset until the wallet has rescanned with the keypool above keypool_min.
  • Add bypasskeypoolcritical (command line argument). This disables the keypool_critical behavior, so the user has a chance to top up their keypool.
  • don't allow user actions like getnewaddress to cause the keypool to drop below the critical limit (return an error telling the user to unlock and topup their keypool).

This is a simpler version of #10830 , which caused the node to stop sync'ing if the keypool dropped below a certain limit. It is built on top of #11022 which does the following:

  • if a key in the keypool is used, mark all keys in the keypool up to that key as used
  • try to top up the keypool when keys from the keypool are used.

This PR couldn't be merged for v0.15 because there are some edge cases that make this dangerous and could result in users not being able to start up the node without onerous recovery steps.

@jnewbery jnewbery mentioned this pull request Jul 19, 2017
3 tasks
super().__init__()
self.setup_clean_chain = True
self.num_nodes = 2
self.extra_args = [['-usehd=0'], ['-usehd=1', '-keypool=100', '-keypoolemin=20']]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

keypoolemin seems misspelled

@instagibbs
Copy link
Member

There really needs to be some sort of recourse presented to the user upon shutdown.

# self.nodes[0].generate(1)
self.sync_all()

assert_equal(self.nodes[1].getbalance(), 15)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On test failure happening this line, I think the reason is that encryptwallet generates an entirely new HD master key, so addr_enc_oldpool and addr_enc_extpool come from a new HD master key which is not part of "hd.bak"

Restoring "hd.enc.bak" instead of "hd.bak" makes this check pass: ryanofsky@93349ad

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for this insight! Yes - that's what was causing the test to fail.

I've made some progress with the test. I've pushed what I've got so far, and hope to finish it off tomorrow morning.

if (id > foundIndex) break; // set*KeyPool is ordered

CKeyPool keypool;
if (!walletdb.ReadPool(id, keypool)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Call seems unnecessary since keypool variable isn't used.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed

LogPrintf("Parameter Interaction: keypool size (%d) must be larger than keypool minimum size for encrypted wallets (%d)\n", keypool_size, keypool_min);
SoftSetArg("-keypool", std::to_string(keypool_min));
}
InitWarning(_("You are using an encrypted HD wallet. You may miss incoming or outgoing transactions."));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW, suggested more detailed wording for the warning message here: #10240 (comment)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated. Let me know what you think of the new text.

InitWarning(_("You are using an encrypted HD wallet. You may miss incoming or outgoing transactions."));
} else {
if (keypool_size < keypool_min && keypool_size < DEFAULT_KEYPOOL_MIN) {
InitWarning(_("Your keypool size is below the recommended limit for HD rescans. You may miss incoming or outgoing transactions."));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested more detailed wording for of this warning, too: #10240 (comment)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated. Let me know what you think of the new text.

}
InitWarning(_("You are using an encrypted HD wallet. You may miss incoming or outgoing transactions."));
} else {
if (keypool_size < keypool_min && keypool_size < DEFAULT_KEYPOOL_MIN) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see any drawbacks to dropping DEFAULT_KEYPOOL_MIN condition here and just treating keypool_min like a normal value the user can control. Keeping this condition might make sense if the && were || (to make warning more paranoid), but currently it seems senseless.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes - fixed

// if the remaining keypool size is below the gap limit, shutdown
LogPrintf("%s: Keypool is too small. Shutting down. internal keypool: %d, external keypool: %d, keypool minimum: %d\n",
__func__, setInternalKeyPool.size(), setExternalKeyPool.size(), keypool_min);
const static std::string error_msg = "Keypool is too small. Shutting down";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Static is a little unusual here, maybe drop it to avoid adding symbols to the binary.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yup

@@ -3523,18 +3576,78 @@ void CReserveKey::ReturnKey()
vchPubKey = CPubKey();
}

void CWallet::CheckKeypoolMinSize() {
unsigned int keypool_min = GetArg("-keypoolmin", DEFAULT_KEYPOOL_MIN);
if (IsHDEnabled() && (setInternalKeyPool.size() < keypool_min || (setExternalKeyPool.size() < keypool_min))) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not 100% clear on this, but if this is an hd wallet, but not an hd-split wallet should we only be checking setExternalKeyPool here because setInternalKeyPool will be empty? See

if (!IsHDEnabled() || !CanSupportFeature(FEATURE_HD_SPLIT))

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you're right. Fixed

* the mostly recently created transactions from newer versions of the wallet.
*/
std::set<CKeyID> keyPool;
GetAllReserveKeys(keyPool);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like this code would more efficient and maybe simpler if instead of making a set of CKeyID's here, we made a map from CKeyID -> (keypool_index, is_internal) and passed it into MarkReserveKeysAsUsed, so the first two loops in that function could be removed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, you're right that this would be much more efficient. However, GetAllReserveKeys() is also called elsewhere, which would also need to be modified to accept a map, so I'd prefer not to change it as part of this PR.

This can be fixed in a follow-up PR unless you think the performance in MarkReserveKeysAsUsed() is unacceptable.

}
}

if (IsHDEnabled() && !TopUpKeyPool()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sipa's comment about only topping up hd key pools would seem to apply here and maybe to CheckKeypoolMinSize: #10240 (review)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. Applies here. I don't think I need to change CheckKeypoolMinSize() though. The node should only be shutdown for HD wallets.

CWalletDB walletdb(*dbw);
for (std::set<int64_t> *setKeyPool : {&setInternalKeyPool, &setExternalKeyPool}) {
int64_t foundIndex = -1;
for (const int64_t& id : *setKeyPool) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe use index instead of id here and other places to distinguish from pool indices from key ids.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@jnewbery
Copy link
Contributor Author

Thanks for the review @ryanofsky . I've addressed all of your concerns except the GetAllReserveKeys() refactor which can be done later.

All the changes are in separate fixup commits which can be squashed later.

@jnewbery
Copy link
Contributor Author

Test is fixed. It required #10703 since the test involves node1 emitting warnings to stderr.

@jnewbery jnewbery changed the title [WIP] Keypool topup Keypool topup Jul 20, 2017
}
walletInstance->TopUpKeyPool();
}
walletInstance->CheckKeypoolMinSize();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to hold the wallet lock here or we fail the assertion at wallet/wallet.cpp:830 in CanSupportFeature.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it ok to lock in CheckKeypoolMinSize() instead?

Copy link
Member

@instagibbs instagibbs Jul 20, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think so. You'd be calling for the lock twice in the other path, but that's ok right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, I don't think there's a problem doing that.

Copy link
Contributor

@ryanofsky ryanofsky left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

utACK 965594a, left minor suggestions but this seems reasonable.

I do think using a map would probably simplify the code (even without changing the GetAllReserveKeys method), not just make it more efficient, so it might be something to reconsider if you end up making more changes for another reason.

Also maybe squash the commit history. I don't think it it's helpful at all. Would just keep the MOVEONLY commit, the #10703 commit, and then combine everything else in another commit.

}
}

auto it = std::begin(*setKeyPool);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe declare closer to while loop, also maybe write it = setKeyPool->begin()

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

}
}

if (!TopUpKeyPool()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe move TopUpKeyPool & CheckKeypoolMinSize out of MarkReserveKeysAsUsed (to the one caller) so MarkReserveKeysAsUsed only does what the name suggests.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

unsigned int keypool_min = GetArg("-keypoolmin", DEFAULT_KEYPOOL_MIN);
if (walletInstance->IsCrypted()) {
if (keypool_size < keypool_min) {
LogPrintf("Parameter Interaction: keypool size (%d) must be larger than keypool minimum size for encrypted wallets (%d)\n", keypool_size, keypool_min);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If keypool size was provided nothing actually happens here so the log message seems misleading. Maybe change SoftSetArg to ForceSetArg or only log this if SoftSetArg returns true.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

LogPrintf("Parameter Interaction: keypool size (%d) must be larger than keypool minimum size for encrypted wallets (%d)\n", keypool_size, keypool_min);
SoftSetArg("-keypool", std::to_string(keypool_min));
}
InitWarning(strprintf(_("You are using an encrypted HD wallet. If you are restoring an old HD wallet that has not been topped up with the most recently "
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe explicitly warn about shutdowns, too

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

self.num_nodes = 2
self.extra_args = [['-usehd=0'], ['-usehd=1', '-keypool=100', '-keypoolmin=20']]

def run_test(self):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe split this up into test_restore(encrypted=False/True) or test_encrypted_restore/test_unencrypted_restore calls to make the test less repetitive and simpler to understand.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@jnewbery
Copy link
Contributor Author

Squashed down to 4 commits. Will address @instagibbs and @ryanofsky feedback next.

@gmaxwell
Copy link
Contributor

Can we make this also suppress the relock while scanning ... so that it's viable to just unlock and have it stay unlocked until its done-ish?

What will this do if, without any rescan, I exhaust all the keys in the wallet... will it falsely trigger?

@laanwj laanwj added this to the 0.15.0 milestone Jul 20, 2017
@@ -3523,18 +3582,68 @@ void CReserveKey::ReturnKey()
vchPubKey = CPubKey();
}

void CWallet::CheckKeypoolMinSize() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Method name should reflect the fact it will shut down upon failure.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggested name?

@jnewbery
Copy link
Contributor Author

I've implemented @ryanofsky's suggestion for simplifying MarkReserveKeysAsUsed() and improved the error message if the node shuts down.

@instagibbs
Copy link
Member

light tACK

Can confirm the new directions upon failure to topup using Crypted lead to recovery of proper index position.

}
walletInstance->TopUpKeyPool();
}
walletInstance->CheckKeypoolMinSize();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new error message doesn't quite work here, but it's also quite unlikely to happen, so maybe moot.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry - I don't understand this comment. Can you explain what you mean by the new error message not quite working?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mistaken, ignore

"You can manually top-up your wallet keypool as follows:\n"
" - restart bitcoin with -keypoolmin set to 0\n"
" - unlock the wallet using walletpassphrase\n"
" - restart with -rescan. This will redownload the blockchain if you are running a pruned node.";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just noting in case I'm underestimating the likelihood/cost: unlikely but this could be painful if they're using more than 1500 keys, as this might trigger multiple times causing them to re-download the chain multiple times.

This will go away with proper long-term fix.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's special about 1500?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok my math is off, I think 1000, I just meant the 2nd time this could occur.

"You can manually top-up your wallet keypool as follows:\n"
" - restart bitcoin with -keypoolmin set to 0\n"
" - unlock the wallet using walletpassphrase\n"
" - restart with -rescan. This will redownload the blockchain if you are running a pruned node.";
Copy link
Contributor

@ryanofsky ryanofsky Jul 20, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


Relatedly, maybe this PR should explicitly avoid calling `WriteBestBlock` when the keypool size is too low, in case shutdown takes a long time or -keypoolmin is set to 0 to recover.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Relatedly, maybe this PR should explicitly avoid calling WriteBestBlock when the keypool size is too low,

ACK. Very much so. If not for having no mechanism for telling you that your wallet has fallen behind, this would more or less eliminate the need for shutdown. though it's weird that you'd lower the minimum to bypass the shutdown and inadvertently update the height.

Copy link
Contributor

@ryanofsky ryanofsky Jul 21, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe there should be two flags:

-keypoolmin as the threshold for calling WriteBestBlock
-keypoolcriticalmin or -keypoolshutdownmin as the threshold for shutting down

Both flags could be set to 500. This still wouldn't provide a safe recovery path that avoids a complete rescan (we would need make walletpassphrase top up and rescan from bestblock after unlocking), but something like that could be added in the future, and in the meantime this would provide a safer way to avoid shutdowns if a rescan can't be done right away.

@jnewbery
Copy link
Contributor Author

Added two commits:

  1. rename current option keypoolcritical - if the keypool falls below this, then shutdown the node
  2. add option keypoolmin - if the keypool falls below this, don't update the wallet's best block.

If people think that's conceptually the right approach, then I'll squash down into sensible commits.

@@ -382,6 +415,11 @@ bool CWallet::ChangeWalletPassphrase(const SecureString& strOldWalletPassphrase,

void CWallet::SetBestChain(const CBlockLocator& loc)
{
unsigned int keypool_min = GetArg("-keypoolmin", DEFAULT_KEYPOOL_MIN);
if (IsHDEnabled() && ((CanSupportFeature(FEATURE_HD_SPLIT) && setInternalKeyPool.size() < keypool_min) || (setExternalKeyPool.size() < keypool_min))) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think there would be drawbacks to dropping the IsHDEnabled check here? Seems like that would be the safer thing to do.

Might also be good to have a helper method to avoid duplicating logic here and in the shutdown code. e.g.:

bool CWallet::HasUnusedKeys(int min_keys) const
{
    return setExternalKeyPool.size() >= min_keys && (setInternalKeyPool.size() >= min_keys || !CanSupportFeature(FEATURE_HD_SPLIT));
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed, if the user is trying to restore from an old non-HD wallet and they drop below keypoolmin/keypoolcritical, there's very little they can do (since topup will generate new random keys). I'll leave this in for now, but if you think we definitely should shutdown or stop advancing best block for non-HD wallets, let me know.

Unless you strongly feel that I should add the helper method, I'll leave this as explicitly coded in both places.

void GetAllReserveKeys(std::set<CKeyID>& setAddress) const;
void CheckKeypoolMinSize();
void MarkReserveKeysAsUsed(const int64_t keypool_index, const bool internal);
void GetAllReserveKeys(std::map<CKeyID, std::pair<int64_t, bool>>& mapKeyPool) const;
Copy link
Contributor

@ryanofsky ryanofsky Jul 21, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would maybe define a little struct here to make code calling this more readable.

struct ReserveKey { int64_t index; bool internal; };
std::map<CKeyID, ReserveKey> GetAllReserveKeys() const;

Returning the map this way would also let callers loop over it easily or use auto.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@@ -3523,30 +3589,65 @@ void CReserveKey::ReturnKey()
vchPubKey = CPubKey();
}

static void LoadReserveKeysToSet(std::set<CKeyID>& setAddress, const std::set<int64_t>& setKeyPool, CWalletDB& walletdb) {
for (const int64_t& id : setKeyPool)
void CWallet::CheckKeypoolMinSize() {
Copy link
Contributor

@ryanofsky ryanofsky Jul 21, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could s/CheckKeypoolMinSize/CheckKeypoolCriticalSize

There was also an old comment about renaming this. #10882 (comment). Could go with something like MaybeShutdownForLaggingWallet

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. Changed name to ShutdownIfKeypoolCritical()

@@ -3834,6 +3903,7 @@ std::string CWallet::GetWalletHelpString(bool showDebug)

strUsage += HelpMessageOpt("-dblogsize=<n>", strprintf("Flush wallet database activity from memory to disk log every <n> megabytes (default: %u)", DEFAULT_WALLET_DBLOGSIZE));
strUsage += HelpMessageOpt("-flushwallet", strprintf("Run a thread to flush wallet periodically (default: %u)", DEFAULT_FLUSHWALLET));
strUsage += HelpMessageOpt("-keypoolcritical", strprintf(_("If the keypool drops below this number of keys and we are unable to generate new keys, shutdown (default: %u)"), DEFAULT_KEYPOOL_CRITICAL));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to add -keypoolmin I think too.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@jnewbery
Copy link
Contributor Author

@ryanofsky I've addressed all of your comments. Let me know if you're happy with the last three commits and I'll squash them down for the benefit of other reviewers,

Copy link
Contributor

@ryanofsky ryanofsky left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

utACK 89af21ddcd7837c45cf8e261711d227e4f8448db

std::map<CKeyID, ReserveKey> CWallet::GetAllReserveKeys() const
{
std::map<CKeyID, ReserveKey> mapKeyPool;
mapKeyPool.clear();
Copy link
Contributor

@ryanofsky ryanofsky Jul 21, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could drop clear, map will already be empty

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dropped

@jnewbery
Copy link
Contributor Author

All comments should now be addressed and changes squashed down into 6 commits:

  • Allow tests to pass when stderr is non-empty is [tests] Allow tests to pass when stderr is non-empty #10703
  • MOVEONLY move CAffectedKeysVisitor is a code move
  • Add ReserveKey struct and return it from GetAllReserveKeys() is a slight refactor of existing code
  • Add keypoolcritical adds the node shutdown functionality
  • Add keypoolmin adds the don't-advance-wallet-best-block functionality
  • add keypool restore functional test adds tests

Ready for wider review.

@jnewbery
Copy link
Contributor Author

jnewbery commented Aug 10, 2017

As requested by @gmaxwell I've pulled out the basic keypool mark-used/topup functionality into #11022. This PR is a superset of that and also contains the keypool_min/keypool_critical functionality to stop the node/prevent best block from advancing if the keypool drops below a threshold.

@jnewbery jnewbery changed the title Keypool topup Stop advancing best block and shutdown node if keypool drops below critical threshold Aug 10, 2017
@ryanofsky
Copy link
Contributor

Note to reviewers, this is now based on #11022, so changes in commits before "Addkeypoolmin" and "Add keypoolcritical" can be discussed there.

@@ -1644,6 +1644,10 @@ CBlockIndex* CWallet::ScanForWalletTransactions(CBlockIndex* pindexStart, bool f

fScanningWallet = false;
}

// Check that we haven't dropped below the keypool_critical threshold.
ShutdownIfKeypoolCritical();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In commit "[wallet] Add keypoolcritical"

I'm confused by this. It seems like this is the only call to ShutdownIfKeypoolCritical() in the whole PR, and it's in the rescan function, so the node will no longer shut down if the wallet is locked and the keypool gradually drains, unless the user manually triggers a rescan or restarts the node? If this is behavior that's intended, it should be documented in the -keypoolcritical help. It might also be good to update the PR description (which is currently out of date) to list the changes here and say what problems are fixed and not fixed by them.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's the current implementation:

07:58 < gmaxwell> jnewbery: Can we distinguish the case where we are current vs rescanning? If so, then we just shouldn't perform the shutdown when we're current.

(https://botbot.me/freenode/bitcoin-core-dev/2017-08-08/?msg=89552785&page=2)

We'll need to discuss this in the meeting today. There doesn't seem to be agreement about what this PR should even be trying to do.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thread #10882 (comment)

That's fine, but it seems that current -keypoolcritical help description is pretty misleading. It would be good to update it, as well as the PR description.

@maflcko maflcko removed this from the 0.15.0 milestone Aug 10, 2017
@maflcko
Copy link
Member

maflcko commented Aug 10, 2017

Cleared the 0.15.0 milestone for now. Let's focus on #11022 first.

@@ -161,8 +161,8 @@ UniValue getnewaddress(const JSONRPCRequest& request)

// Generate a new key that is added to wallet
CPubKey newKey;
if (!pwallet->GetKeyFromPool(newKey)) {
throw JSONRPCError(RPC_WALLET_KEYPOOL_RAN_OUT, "Error: Keypool ran out, please call keypoolrefill first");
if (!pwallet->GetKeyFromPool(newKey, false, true)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In commit "[wallet] Don't allow getnewaddress to drop keypool to critical."

Do you want to make the same change to getrawchangeaddress? (as mentioned #10882 (comment))

@@ -1644,6 +1644,10 @@ CBlockIndex* CWallet::ScanForWalletTransactions(CBlockIndex* pindexStart, bool f

fScanningWallet = false;
}

// Check that we haven't dropped below the keypool_critical threshold.
ShutdownIfKeypoolCritical();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thread #10882 (comment)

That's fine, but it seems that current -keypoolcritical help description is pretty misleading. It would be good to update it, as well as the PR description.

if (walletInstance->IsHDEnabled()) {
unsigned int keypool_size = GetArg("-keypool", DEFAULT_KEYPOOL_SIZE);
unsigned int keypool_critical = GetArg("-keypoolcritical", DEFAULT_KEYPOOL_CRITICAL);
if (keypool_size < keypool_critical) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thread #10882 (comment)

I don't see a change, should log print just be changed to "smaller or equal to"?

if (IsHDEnabled() && !HasUnusedKeys(keypool_min) && m_update_best_block) {
LogPrintf("Keypool has fallen below keypool_min (%s). Wallet will no longer watch for new transactions and best block height will not be advanced.\n", keypool_min);
LogPrintf("Unlock wallet, top up keypool and rescan to resume watching for new transactions.\n");
m_update_best_block = false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In commit "[wallet] Add m_update_best_block"

As mentioned #10882 (comment), I think the transition from true -> false m_update_best_block should just happen after the topup in AddToWalletIfInvolvingMe. So this logic, and the logic in SetBestChain could be removed. SetBestChain would only read m_update_best_block, not write to it or interact with the keypool.

laanwj added a commit that referenced this pull request Aug 14, 2017
d34957e [wallet] [tests] Add keypool topup functional test (Jonas Schnelli)
095142d [wallet] keypool mark-used and topup (John Newbery)
c25d90f [wallet] Add HasUnusedKeys() helper (John Newbery)
f2123e3 [wallet] Cache keyid -> keypool id mappings (John Newbery)
83f1ec3 [wallet] Don't hold cs_LastBlockFile while calling setBestChain (John Newbery)
2376bfc [wallet] [moveonly] Move LoadKeyPool to cpp (Matt Corallo)
cab8557 [wallet] [moveonly] Move CAffectedKeysVisitor (Jonas Schnelli)

Pull request description:

  This PR contains the first part of #10882 :

  - if a key from the keypool is used, mark all keys up to that key as used, and then try to top up the keypool
  - top up the keypool on startup

  Notably, it does not stop the node or prevent the best block from advancing if the keypool drops below a threshold (which means that transactions may be missed and funds lost if restoring from an old HD wallet backup).

Tree-SHA512: ac681fefeaf7ec2aab2fa1da93d12273ea80bd05eb48d7b3b551ea6e5d975dd97ba7de52b7fba52993823280ac4079cc36cf78a27dac708107ebf8fb6326142b
Add a -keypoolcritical option. If an HD wallet's keypool drops below
this size and can't be topped up (since the wallet is encrypted), stop
the node.
If an HD wallet's keypool falls below keypoolmin, don't update its best
block until the keypool has been topped up.
m_update_best_block is set to false the first time we try to
SetBestChain and the keypool has falled below keypool_min. After that we
won't try to update the wallet's best block until we've rescanned from
that point with the keypool above keypool_min.
bypasskeypoolcritical is a hidden command line option that allows the
user to restart bitcoin with an encrypted wallet below the
keypool_critical threshold. Bitcoind won't shut down, but it also won't
advance the wallet best block until the keypool is topped up.
Adds a bool parameter to GetKeyFromPool(), which will cause the function
to return failure if taking a key from the keypool would result in
hitting the keypool_critical threshold.
If getting a new address would cause the keypool to drop below the
keypool_critical threshold, fail the RPC and prompt the user to unlock
the wallet so the keypool can be topped up.
In almost all cases, a call to GetKeyFromPool() should fail if getting a
new key would result in the keypool dropping below critical.
This updates the keypool-topup.py test script to test the keypool
critical functionality (ie the node shuts down if the keypool drops
below the keypool_critical threshold and the wallet can't topup its
keypool)
@jnewbery
Copy link
Contributor Author

rebased on master and updated PR notes. Shouldn't be merged without further work to make sure this is safe.

@gmaxwell - could you comment here on the edge cases here (for future reference).

@jnewbery jnewbery changed the title Stop advancing best block and shutdown node if keypool drops below critical threshold [Do not merge] Stop advancing best block and shutdown node if keypool drops below critical threshold Aug 14, 2017
@jnewbery
Copy link
Contributor Author

Closing for now. There were several edge-cases discovered that meant that this approach needs a bit more careful consideration. @gmaxwell is going to add some notes to this PR so if we do pick this later, we don't need to rediscover the edge-cases.

@droark
Copy link
Contributor

droark commented Dec 9, 2017

@jnewbery - Just happened to come across this PR while going through some commits. Did you ever get the edge case notes?

@jnewbery
Copy link
Contributor Author

jnewbery commented Dec 9, 2017

I'm not sure if Greg collected them together. You can probably find them if you scrape through the IRC logs.

PastaPastaPasta pushed a commit to PastaPastaPasta/dash that referenced this pull request Sep 18, 2019
d34957e [wallet] [tests] Add keypool topup functional test (Jonas Schnelli)
095142d [wallet] keypool mark-used and topup (John Newbery)
c25d90f [wallet] Add HasUnusedKeys() helper (John Newbery)
f2123e3 [wallet] Cache keyid -> keypool id mappings (John Newbery)
83f1ec3 [wallet] Don't hold cs_LastBlockFile while calling setBestChain (John Newbery)
2376bfc [wallet] [moveonly] Move LoadKeyPool to cpp (Matt Corallo)
cab8557 [wallet] [moveonly] Move CAffectedKeysVisitor (Jonas Schnelli)

Pull request description:

  This PR contains the first part of bitcoin#10882 :

  - if a key from the keypool is used, mark all keys up to that key as used, and then try to top up the keypool
  - top up the keypool on startup

  Notably, it does not stop the node or prevent the best block from advancing if the keypool drops below a threshold (which means that transactions may be missed and funds lost if restoring from an old HD wallet backup).

Tree-SHA512: ac681fefeaf7ec2aab2fa1da93d12273ea80bd05eb48d7b3b551ea6e5d975dd97ba7de52b7fba52993823280ac4079cc36cf78a27dac708107ebf8fb6326142b
PastaPastaPasta pushed a commit to PastaPastaPasta/dash that referenced this pull request Sep 20, 2019
d34957e [wallet] [tests] Add keypool topup functional test (Jonas Schnelli)
095142d [wallet] keypool mark-used and topup (John Newbery)
c25d90f [wallet] Add HasUnusedKeys() helper (John Newbery)
f2123e3 [wallet] Cache keyid -> keypool id mappings (John Newbery)
83f1ec3 [wallet] Don't hold cs_LastBlockFile while calling setBestChain (John Newbery)
2376bfc [wallet] [moveonly] Move LoadKeyPool to cpp (Matt Corallo)
cab8557 [wallet] [moveonly] Move CAffectedKeysVisitor (Jonas Schnelli)

Pull request description:

  This PR contains the first part of bitcoin#10882 :

  - if a key from the keypool is used, mark all keys up to that key as used, and then try to top up the keypool
  - top up the keypool on startup

  Notably, it does not stop the node or prevent the best block from advancing if the keypool drops below a threshold (which means that transactions may be missed and funds lost if restoring from an old HD wallet backup).

Tree-SHA512: ac681fefeaf7ec2aab2fa1da93d12273ea80bd05eb48d7b3b551ea6e5d975dd97ba7de52b7fba52993823280ac4079cc36cf78a27dac708107ebf8fb6326142b
PastaPastaPasta pushed a commit to PastaPastaPasta/dash that referenced this pull request Sep 20, 2019
d34957e [wallet] [tests] Add keypool topup functional test (Jonas Schnelli)
095142d [wallet] keypool mark-used and topup (John Newbery)
c25d90f [wallet] Add HasUnusedKeys() helper (John Newbery)
f2123e3 [wallet] Cache keyid -> keypool id mappings (John Newbery)
83f1ec3 [wallet] Don't hold cs_LastBlockFile while calling setBestChain (John Newbery)
2376bfc [wallet] [moveonly] Move LoadKeyPool to cpp (Matt Corallo)
cab8557 [wallet] [moveonly] Move CAffectedKeysVisitor (Jonas Schnelli)

Pull request description:

  This PR contains the first part of bitcoin#10882 :

  - if a key from the keypool is used, mark all keys up to that key as used, and then try to top up the keypool
  - top up the keypool on startup

  Notably, it does not stop the node or prevent the best block from advancing if the keypool drops below a threshold (which means that transactions may be missed and funds lost if restoring from an old HD wallet backup).

Tree-SHA512: ac681fefeaf7ec2aab2fa1da93d12273ea80bd05eb48d7b3b551ea6e5d975dd97ba7de52b7fba52993823280ac4079cc36cf78a27dac708107ebf8fb6326142b
barrystyle pushed a commit to PACGlobalOfficial/PAC that referenced this pull request Jan 22, 2020
d34957e [wallet] [tests] Add keypool topup functional test (Jonas Schnelli)
095142d [wallet] keypool mark-used and topup (John Newbery)
c25d90f [wallet] Add HasUnusedKeys() helper (John Newbery)
f2123e3 [wallet] Cache keyid -> keypool id mappings (John Newbery)
83f1ec3 [wallet] Don't hold cs_LastBlockFile while calling setBestChain (John Newbery)
2376bfc [wallet] [moveonly] Move LoadKeyPool to cpp (Matt Corallo)
cab8557 [wallet] [moveonly] Move CAffectedKeysVisitor (Jonas Schnelli)

Pull request description:

  This PR contains the first part of bitcoin#10882 :

  - if a key from the keypool is used, mark all keys up to that key as used, and then try to top up the keypool
  - top up the keypool on startup

  Notably, it does not stop the node or prevent the best block from advancing if the keypool drops below a threshold (which means that transactions may be missed and funds lost if restoring from an old HD wallet backup).

Tree-SHA512: ac681fefeaf7ec2aab2fa1da93d12273ea80bd05eb48d7b3b551ea6e5d975dd97ba7de52b7fba52993823280ac4079cc36cf78a27dac708107ebf8fb6326142b
@bitcoin bitcoin locked as resolved and limited conversation to collaborators Sep 8, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.