Add `-disablehot` mode: a sane mode for watchonly-wallets #9662

Open
wants to merge 6 commits into
from

Conversation

Projects
None yet
9 participants
Member

jonasschnelli commented Feb 1, 2017

This mode (-disablehot) is intended for a sane pure watch-only mode, ideal for a use-case where one likes to use Bitcoin-Core in conjunction with a hardware-wallet or another solutions for cold-stogare.

Since we have support for custom change addresses in fundrawtransaction, pure watch-only wallets including coin-selection are possible and do make sense for some use cases.

This new mode disables all forms of private key generation and ensure that no mix between hot and cold keys are possible.

jonasschnelli added the Wallet label Feb 1, 2017

Contributor

TheBlueMatt commented Feb 1, 2017 edited

It seems somewhat strange that this is set per-run and not per-wallet. Indeed, if it were set per-wallet, an easy way to get the intended behavior here is to simply encrypt your wallet with a garbage passphrase such that it can no longer be opened (not to say we shouldn't do this, better UX around that is good, but it might be easier to review/write if it re-used that infrastructure).

Owner

laanwj commented Feb 2, 2017

Concept ACK. I agree that this would be better as a mode on the wallet instead of yet another startup option.

Member

NicolasDorier commented Feb 16, 2017 edited

I would like to rebase on top of that for #9728, can you rebase?
I would switch that on if hdwatchonly is used.

Member

jonasschnelli commented Feb 17, 2017

Added two commit.

  • First commit adds a facility to store and check wallet flags (64bit). Made DISABLE_PRIVATE_KEYS the first flag.
  • Second commit makes the disablehot function per wallet (no longer a global state)

Disabling private keys on a wallet that already contains private keys is not possible (also the other way around).

qa/rpc-tests/disablehot.py
+ print("restarting node with disablehot=0 (try to re-enable private keys which must not be possible)")
+ stop_nodes(self.nodes)
+ try:
+ self.nodes = start_nodes(self.num_nodes, self.options.tmpdir, [[], ['-disablehot=0']])
@jonasschnelli

jonasschnelli Feb 17, 2017

Member

@MarcoFalke: any idea how I can make this pass? Right now, staring bitcoind in this case with this setup will cause bitcoind to write to stderr (and halt) which then result in pass=false in rpc-tests.py. But it's actually the correct behaviour.

@MarcoFalke

MarcoFalke Feb 18, 2017

Member

We might create a assert_start_raises_init_error() method. I will take a look at this tomorrow.

src/wallet/wallet.cpp
@@ -1408,13 +1407,25 @@ bool CWallet::IsHDEnabled()
return !hdChain.masterKeyID.IsNull();
}
+void CWallet::SetWalletFlag(uint64_t flag)
+{
+ walletFlags |= flag;
@NicolasDorier

NicolasDorier Feb 20, 2017 edited

Member

Kind of confusing....
I thought this would be equivalent to CWallet::SetWalletFlags(uint64_t flags, false).

However while CWallet::SetWalletFlags(uint64_t flags, bool memonly) set the flags to the indicated value, CWallet::SetWalletFlag(uint64_t flag) appends to it.

Maybe you should rename to AddWalletFlags.

@saleemrashid

saleemrashid Feb 20, 2017

CWallet::AddWalletFlags(uint64_t flags, book memonly) seems most consistent.

@jonasschnelli

jonasschnelli Feb 21, 2017

Member

IMO set flags seems most appropriate here. Flags are not getting appended. Either you set or unset IMO. For consistency, we should have a UnsetWalletFlag().
But I agree its kind of confusing that we have a SetWalletFlags (plural) method that is capable or re-setting all 64 flags.

@saleemrashid

saleemrashid Feb 21, 2017

I think this method should be plural, since there is no distinction between a single flag and multiple flags in its operation and it might be useful to be able to do the latter.

Then, of course, you have confusion. So, a verb change in the method name might be useful.

@NicolasDorier

NicolasDorier Feb 22, 2017 edited

Member

@jonasschnelli the strange thing is that SetWalletFlag switch a bit, when SetWalletFlags set the walletFlags to a specific value. I was expecting SetWalletFlags to switch several bits.

@jonasschnelli

jonasschnelli Feb 22, 2017

Member

Maybe renaming SetWalletFlags (plural) to OverwriteWalletFlags?

@ryanofsky

ryanofsky May 4, 2017

Contributor

Maybe renaming SetWalletFlags (plural) to OverwriteWalletFlags?

Since this is doing an | I'd call it AddFlags (which conceivably could be paired with a RemoveFlags doing & ~).

qa/rpc-tests/disablehot.py
+ self.num_nodes = 2
+
+ def setup_network(self, split=False):
+ self.nodes = start_nodes(self.num_nodes, self.options.tmpdir, [[], ['-disablehot']])
@NicolasDorier

NicolasDorier Feb 20, 2017

Member

does the [] arguments is on purpose? because I think it makes -disablehot ignored

@jonasschnelli

jonasschnelli Feb 21, 2017

Member

We start two nodes here, the first node without startup parameters (thats why there is the []). Only second node has -disablehot.

Member

jonasschnelli commented Mar 3, 2017

Needed rebase (#8775).
Travis will fail because of the missing assert_start_raises_init_error (see #9832).

Owner

laanwj commented Mar 6, 2017 edited

ideal for a use-case where one likes to use Bitcoin-Core in conjunction with a hardware-wallet or another solutions for cold-stogare

Nice, so this guarantees that bitcoind process will contain no private key data at all?
Seems like s a good step towards supporting hardware wallets too.
Concept ACK.

Travis should pass now that #9832 is merged.

Member

jonasschnelli commented Mar 6, 2017

The disablehot.py now makes use of the new (#9832) assert_start_raises_init_error call.

Nice, so this guarantees that bitcoind process will contain no private key data at all?

Yes. Event the default key is disabled (while still detecting the first start correctly).

+ else if (IsArgSet("-disablehot")) {
+ bool disableHot = GetBoolArg("-disablehot", DEFAULT_DISABLE_HOT_WALLET);
+ LOCK(walletInstance->cs_wallet);
+ if (walletInstance->vchDefaultKey.IsValid() && disableHot) {
@NicolasDorier

NicolasDorier Mar 13, 2017

Member

I think you should use fFirstRun here instead of walletInstance->vchDefaultKey.IsValid()

@jonasschnelli

jonasschnelli Mar 13, 2017

Member

At this point fFirstRun is always false. And I think this also works in conjunction with salvagewallet.

@NicolasDorier

NicolasDorier Mar 13, 2017

Member

Indeed. Might be out of this PR matter, in the case of hdwatchonly, walletInstance->vchDefaultKey.IsValid() will be true. So this would mean disabledHot is incompatible with hdwatchonly mode ?

@NicolasDorier

NicolasDorier Mar 13, 2017

Member

OK forget what I said, there is no reason to have disabledHot with hdwatchonly at same time.

Member

jonasschnelli commented Mar 29, 2017

Rebased.

Owner

laanwj commented May 2, 2017

Needs rebase again (sorry)
utACK otherwise.

Member

jonasschnelli commented May 4, 2017

Rebased.

@ryanofsky

utACK 9b39bf9

The commit history of this PR is messy though. I would suggest moving the "Add facility to store wallet flags (64 bits)" commit to be first, then then squashing all the other changes down into single commit following that.

src/wallet/wallet.cpp
@@ -2497,6 +2500,11 @@ bool CWallet::CreateTransaction(const std::vector<CRecipient>& vecSend, CWalletT
// post-backup change.
// Reserve a new key pair from key pool
+ if (fDisableHotKeys) {
@ryanofsky

ryanofsky May 4, 2017

Contributor

In commit "Add -disablehot option to ensure pure watchonly-wallets"

Do you think it might be better to fail earlier in CreateTransaction, regardless of the nChange > 0 condition? I'm thinking it might be, just so calls would fail consistently instead of sometimes randomly succeeding. But I don't have much understanding of the real-world use-cases for this, so this is just a thought.

Considering adding a code comment here explaining more.

@jonasschnelli

jonasschnelli May 5, 2017

Member

Falling earlier would disallow creating watch-only transactions (over fundrawtransaction).

src/wallet/wallet.cpp
@@ -1408,13 +1407,25 @@ bool CWallet::IsHDEnabled()
return !hdChain.masterKeyID.IsNull();
}
+void CWallet::SetWalletFlag(uint64_t flag)
+{
+ walletFlags |= flag;
@ryanofsky

ryanofsky May 4, 2017

Contributor

Maybe renaming SetWalletFlags (plural) to OverwriteWalletFlags?

Since this is doing an | I'd call it AddFlags (which conceivably could be paired with a RemoveFlags doing & ~).

src/wallet/wallet.cpp
@@ -3915,6 +3948,7 @@ bool CWallet::ParameterInteraction()
nTxConfirmTarget = GetArg("-txconfirmtarget", DEFAULT_TX_CONFIRM_TARGET);
bSpendZeroConfChange = GetBoolArg("-spendzeroconfchange", DEFAULT_SPEND_ZEROCONF_CHANGE);
fWalletRbf = GetBoolArg("-walletrbf", DEFAULT_WALLET_RBF);
+ fDisableHotKeys = GetBoolArg("-disablehot", DEFAULT_DISABLE_HOT_WALLET);
@ryanofsky

ryanofsky May 4, 2017

Contributor

In commit "Add -disablehot option to ensure pure watchonly-wallets"

I don't understand why we want to call these "hot keys" and not "private keys," but I guess I'm out of the loop on terminology not knowing precisely what a "hot key" is.

src/wallet/wallet.cpp
+ }
+ bool hotKeys = walletInstance->IsWalletFlagSet(WALLET_FLAG_DISABLE_HOT_KEYS);
+ if (hotKeys && !disableHot) {
+ InitError(strprintf(_("Error loading %s: You can't enable hot keys once you have initialized a wallet with disabled hot keys"), walletFile));
@ryanofsky

ryanofsky May 4, 2017

Contributor

Add -disablehot mode: a sane mode for watchonly-wallets

Maybe the error message could suggest passing -disablehot to avoid the problem. I think it would make the context of the error easier to understand for someone not expecting it.

qa/rpc-tests/disablehot.py
+ self.nodes[1].getnewaddress()
+ raise AssertionError("Wallet can derive private keys while -disablehot is enabled")
+ except JSONRPCException as e:
+ assert('Hot key' in e.error['message'])
@ryanofsky

ryanofsky May 4, 2017

Contributor

In commit "Add -disablehot option to ensure pure watchonly-wallets"

Consider using assert_raises_jsonrpc. (It's less verbose)

src/qt/walletmodel.cpp
@@ -705,7 +705,8 @@ bool WalletModel::hdEnabled() const
bool WalletModel::hotKeysDisabled() const
{
- return fDisableHotKeys;
+ return (pwalletMain->IsWalletFlagSet(WALLET_FLAG_DISABLE_HOT_KEYS));
+
@ryanofsky

ryanofsky May 4, 2017

Contributor

In commit "Make -disablehot per wallet (remove global state)"

Extra newline here

jonasschnelli added some commits Feb 16, 2017

@jonasschnelli jonasschnelli Add facility to store wallet flags (64 bits) 2fde1cf
@jonasschnelli jonasschnelli Add per wallet -disablehot option to ensure pure watchonly-wallets 9935fbd
@jonasschnelli jonasschnelli [Qt] Disable creating receive addresses when -disablehot is set 885e991
@jonasschnelli jonasschnelli [QA] Add disablehot.py functional test fcea55a
Member

jonasschnelli commented May 5, 2017

  • Completely rewrote the commit history (as reported by @ryanofsky it was messy).
  • Fixed @ryanofsky points

No strong opinion if this should be called -disableprivatekeys (which somehow nails it but may confuse people who just want cold key storage). Other opinions?�

Contributor

ryanofsky commented May 5, 2017

utACK fcea55a

Changes from previous review: pwalletMain removal, SetWalletFlag rename, error message tweaks, assert_raises_jsonrpc usage, and history cleanup.

Member

NicolasDorier commented May 6, 2017

utACK fcea55a

Contributor

ryanofsky commented May 23, 2017

This has 3 utACKs so I'm building it now to provide a little testing. Maybe this PR should also update doc/release-notes.md since it's adding a new option.

On "private keys" vs "hot keys" naming, "private keys" still sounds more comprehensible to me, but I wouldn't weigh my opinion very heavily if "hot key" is a term users will know.

Contributor

ryanofsky commented May 23, 2017

Tested ACK fcea55a. Confirmed disablehot option prevents generating keys, persists across reload even when disablehot option not specified in later runs, and that it prevents loading pre-existing or non-disablehot wallets. I don't think I encountered any bugs, and it seems to me this PR works well and is safe to merge.

I was surprised by two things, and maybe these behaviors could be changed in the future if they aren't intentional:

  • I was surprised that -disablehot could only be used on a new wallet and would refuse to load a preexisting wallet that did not have any private keys.
  • I was surprised that -disablehot did seem to allow importing private keys via importprivkey and importwallet rpcs.

jonasschnelli added some commits May 24, 2017

@jonasschnelli jonasschnelli Disable importprivkey and importwallet during -disablehot mode 770db3a
@jonasschnelli jonasschnelli Use pwallet instead of pwalletMain for IsWalletFlagSet tests 9e94cc4
Member

jonasschnelli commented May 24, 2017

Added two commits.
The first addresses the "importprivkey" / "Importwallet" issues reported by @ryanofsky (disable both commands in -disablehot mode).
The second fixes a rebase pwallet/pwalletmain issue.

@ryanofsky

utACK 9e94cc4. Did not retest, but the two new commits are straightforward and look good. This needs rebase, and the last pwalletMain fixing commit should probably be squashed into the main "Add per wallet -disablehot" comment.

Member

instagibbs commented Jun 19, 2017

needs rebase, will review

Member

instagibbs commented Jun 19, 2017

General comment while I wait for rebase: This is fundamentally in conflict with the hdwatchonly mode proposed in #9728 which was surprising to me. Perhaps the descriptions of the mode should stress that no (pub)key generation of any sort is allowed, or explicitly state that only imported watchonly keys are supported?

Also, I think to make this mode less brittle if an assert in AddKeyPubKey is added, since this should never be reachable.

@@ -2833,6 +2839,9 @@ DBErrors CWallet::LoadWallet(bool& fFirstRunRet)
if (nLoadWalletRet != DB_LOAD_OK)
return nLoadWalletRet;
fFirstRunRet = !vchDefaultKey.IsValid();
+ if (IsWalletFlagSet(WALLET_FLAG_DISABLE_HOT_KEYS)) {
@instagibbs

instagibbs Jun 19, 2017

Member

Maybe leave a comment here to make it clear there are two modes of detecting an initialized wallet here.

@jnewbery

jnewbery Jul 10, 2017

Member

Agree, and combine the two conditionals to make it clearer that that's what is happening.

fFirstRunRet = !vchDefaultKey.IsValid() && !IsWalletFlagSet(WALLET_FLAG_DISABLE_HOT_KEYS);
@@ -2957,6 +2966,9 @@ bool CWallet::SetDefaultKey(const CPubKey &vchPubKey)
*/
bool CWallet::NewKeyPool()
{
+ if (IsWalletFlagSet(WALLET_FLAG_DISABLE_HOT_KEYS)) {
@instagibbs

instagibbs Jun 19, 2017

Member

Return condition for this function is never checked. Only used here: https://github.com/bitcoin/bitcoin/blob/master/src/wallet/wallet.cpp#L639

For this mode do we even need the encrypt functionality?

@jnewbery

Generally looks good. A few comments inline. I haven't tested yet other than running the new functional test. I'll do some manual testing tomorrow.

I like the flag infrastructure you've put in place. I wonder if it can be used in #7729 as a way to upgrade from accounts to labels.

It'd be good to add the flags to the getwalletinfo output, but that can be done in a future PR.

I have a very slight preference for naming this -disableprivatekeys, but -disablehot is also fine.

@@ -3579,6 +3617,7 @@ std::string CWallet::GetWalletHelpString(bool showDebug)
{
std::string strUsage = HelpMessageGroup(_("Wallet options:"));
strUsage += HelpMessageOpt("-disablewallet", _("Do not load the wallet and disable wallet RPC calls"));
+ strUsage += HelpMessageOpt("-disablehot", _("Disable the possibility of hot keys (only watchonlys are possible in this mode") + " " + strprintf(_("(default: %u)"), DEFAULT_DISABLE_HOT_WALLET));
@jnewbery

jnewbery Jul 10, 2017

Member

nit: place alphabetically (above -disablewallet)

+ LOCK(walletInstance->cs_wallet);
+ /* make sure we don't have hot keys */
+ if (walletInstance->GetKeyPoolSize()) {
+ InitError(strprintf(_("Error loading %s: You can't disable hot keys if your wallet already contains hot keys"), walletFile));
@jnewbery

jnewbery Jul 10, 2017

Member

This seems like the wrong error message: You can't disable hot keys... when the user has not specified the -disablehot parameter. Rather, this branch indicates some kind of wallet corruption: the DISABLE_HOT_KEYS flag is set, but the wallet contains private keys.

@@ -0,0 +1,67 @@
+#!/usr/bin/env python3
@jnewbery

jnewbery Jul 10, 2017

Member

Consider running a linter like flake8 over python modules to check for common nits.

+# Copyright (c) 2017 The Bitcoin Core developers
+# Distributed under the MIT software license, see the accompanying
+# file COPYING or http://www.opensource.org/licenses/mit-license.php.
+
@jnewbery

jnewbery Jul 10, 2017

Member

Can you add a short docstring explaining what the test is testing and how it is testing?

+ self.setup_clean_chain = True
+ self.num_nodes = 2
+
+ def setup_network(self, split=False):
@jnewbery

jnewbery Jul 10, 2017

Member

You can avoid having to override this method by setting:

self.extra_args = [[], ['-disablehot']]

in the __init__() method

+ def run_test(self):
+ assert_equal(self.nodes[1].getwalletinfo()['keypoolsize'], 0)
+
+ print("Mining blocks...")
@jnewbery

jnewbery Jul 10, 2017

Member

please replace print() calls with self.log.info()

+ n0addrC = self.nodes[0].getnewaddress()
+ n0pubkC = self.nodes[0].validateaddress(n0addrC)['pubkey']
+ n0addr = self.nodes[0].getnewaddress()
+ self.nodes[0].sendtoaddress(n0addrR, 10);
@jnewbery

jnewbery Jul 10, 2017

Member

no ; please 🙂

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment