Can create Watch Only HD wallet with -hdwatchonly #9728

Open
wants to merge 6 commits into
from

Conversation

Member

NicolasDorier commented Feb 9, 2017 edited

The user creates a new wallet by running ./bitcoind -hdwatchonly=[ExtPubKey base58].

This make it possible to use methods like getnewaddress, fundrawtransaction and all normal wallet operations on a HD pubkey.

Software built on top of core which need to delegate signing operations to hardware wallet will have almost the same code as if signing was done by Core.

With the introduction of a standard for dealing with hardware wallet signing in the future, I expect that signrawtransaction will just delegate the signing to the hardware wallet.

In this way, there will be no code difference between software using third party solution for signing, and those just using core for signing.

I will use it in my own projects. My HW is giving me the ExtPubKey, and I want to use bitcoin core just for coin selection and tracking. I also did not wanted to break bunch of old code. Ping @jonasschnelli

EDIT: @saleemrashid built HW support for Bitcoin Core on https://github.com/saleemrashid/bitcoin/tree/hardware-wallet based on this PR

src/wallet/walletdb.h
@@ -59,6 +63,10 @@ class CHDChain
READWRITE(this->nVersion);
READWRITE(nExternalChainCounter);
READWRITE(masterKeyID);
+ if (this->nVersion <= SUPPORT_WATCHONLY_VERSION) {
@jonasschnelli

jonasschnelli Feb 9, 2017

Member

>=?

src/wallet/wallet.cpp
+ }
+ else {
+ if (!AddWatchOnly(GetScriptForDestination(pubkey.GetID())))
+ throw std::runtime_error(std::string(__func__) + ": AddKey failed");
@jonasschnelli

jonasschnelli Feb 9, 2017

Member

s/AddKey/AddWatchOnly/

src/wallet/wallet.cpp
+ // update the chain model in the database
+ if (!CWalletDB(strWalletFile).WriteHDChain(hdChain))
+ throw std::runtime_error(std::string(__func__) + ": Writing HD chain model failed");
+ return false;
@jonasschnelli

jonasschnelli Feb 9, 2017

Member

I think returning false in case of isWatchOnly is confusing.
IMO better set the hasSecret boolean with another check of hdChain.isWatchOnly.

@NicolasDorier

NicolasDorier Feb 9, 2017 edited

Member

I can replace returns bool by void, and make the client responsible to test the validity of the returned Key.

But I fear that a new user would assume that DeriveNewKey always returns a valid CKey.

@jonasschnelli

jonasschnelli Feb 9, 2017

Member

Yes. I once did a PR where we separate the key/pubkey records: #9298
I guess this would be the cleaner solution... but maybe later.

Member

jonasschnelli commented Feb 9, 2017

Concept ACK (will review in detail and test soon).
I think this is a great feature!
Together with #9662 this would allow better HWW/Cold-Storage interaction.

fanquake added the Wallet label Feb 9, 2017

Owner

laanwj commented Feb 9, 2017

The user create a new wallet by removing the old wallet.dat and running ./bitcoind -hdwatchonly=[ExtPubKey base58].

Instead of removing the wallet, it would also be possible to specify an alternative one with -wallet, I guess?

Member

luke-jr commented Feb 9, 2017

How does this work, considering that Core is exclusively hardened derivation right now? Or can it only watch non-Core wallets? O.o

Member

jonasschnelli commented Feb 9, 2017

How does this work, considering that Core is exclusively hardened derivation right now? Or can it only watch non-Core wallets? O.o

For the hd-watch-only default keypath, we should probably use Bip44.
This PR makes much more sense if we would have the flexible key path feature #8723

Member

NicolasDorier commented Feb 9, 2017

@luke-jr for watchonly hd I am using non hardened derivation. goal is to eventually combine with flexible path.

@laanwj yes. What I mean is that if you specify -hdwatchonly on an already existing wallet, you get an error at startup.

Member

NicolasDorier commented Feb 9, 2017

Travis error not related to this PR.

Member

jonasschnelli commented Feb 9, 2017

@NicolasDorier: I think in order to pass travis you need to add -hdwatchonly to the check doc script.

Member

NicolasDorier commented Feb 15, 2017 edited

I added a commit to track P2PK as well. It would be the same behavior as normal wallet. However I am not sure it is the right approach as P2PK are obsolete. An attacker could disrupt service by sending a P2PK output, when the service does not expect it.

Also, what for P2WPKH ?

Member

NicolasDorier commented Feb 20, 2017

I am replicating the behavior of normal wallets, both p2pk and p2pkh are tracked.

The test is failing because I am testing wrong parameter usage, and there is no way in the test framework to assert an exception. Ping @MarcoFalke , same problem as reported by jonas on #9662

qa/rpc-tests/test_framework/util.py
+ assert('bitcoind exited' in str(e)) #node must have shutdown
+
+
+def start_nodes(num_nodes, dirname, extra_args=None, rpchost=None, timewait=None, binary=None, ignorestderr=False):
@MarcoFalke

MarcoFalke Feb 21, 2017 edited

Member

Is this additional optional arg required? I don't think this will ever be used.

Edit: I see you are doing it for consistency, so might be fine...

@NicolasDorier

NicolasDorier Feb 21, 2017

Member

it is used by assert_start_raises_init_error

@MarcoFalke

MarcoFalke Feb 21, 2017

Member

Oh, right. I missed the plural s.

Member

NicolasDorier commented Feb 21, 2017 edited

I am at loss understanding why there is a permission denied on travis.... I thought I maybe did not had right to /dev/null, and tried to redirect stderr to stdout instead, but same problem. Any idea ?

Member

MarcoFalke commented Feb 21, 2017

Any idea

Try

chmod +x qa/rpc-tests/hdwatchonly.py
Member

NicolasDorier commented Mar 13, 2017

rebased, and cleaned up the commits for better review.

As far as I understand, Bitcoin Core uses m/0'/0'/k' whereas BIP 44 uses m/0'/0/k and m/0'/1/k. It makes most sense for this PR to implement m/0/k (and, with #9294, m/1/k). This allows you to use an xpub derived from m/0' (or any other account) by your hardware wallet, for example.

However, this means that you are doing one less derivation than with xprv in Bitcoin Core. I don't think this is too much of an issue since m/0'/0'/k' is in no way compatible with m/0'/0/k anyway.

Member

NicolasDorier commented Mar 14, 2017

@saleemrashid very good point. Will make your change once #9294 is merged.

src/wallet/walletdb.h
- static const int CURRENT_VERSION = 1;
+ static const int FIRST_VERSION = 1;
+ static const int SUPPORT_WATCHONLY_VERSION = 2;
+ static const int CURRENT_VERSION = 2;
@jonasschnelli

jonasschnelli Mar 17, 2017

Member

nit: CURRENT_VERSION = SUPPORT_WATCHONLY_VERSION.

src/wallet/wallet.cpp
+ }
+ else if (IsHDWatchOnly()) {
+ CExtPubKey& masterKey = hdChain.masterPubKey; //hd master key
+ CExtPubKey accountKey; //key at m/0
@jonasschnelli

jonasschnelli Mar 17, 2017

Member

To make this compatible with BIP44, can we not just use m as the BIP44 account key. No hardened derivation would then be involved and users could use their BIP44 xpub's to generate watchonlys.
The externalChainChildKey would then be m/0 (instead of m/0/0)

@jonasschnelli

jonasschnelli Mar 20, 2017

Member

Ah.. Wasn't aware that @saleemrashid already pointed this out. Great.

src/wallet/wallet.cpp
+ CExtPubKey childKey; //key at m/0/0/<n>
+
+ // derive m/0
+ masterKey.Derive(accountKey, BIP32_NONHARDENED_KEY_LIMIT);
@jonasschnelli

jonasschnelli Mar 17, 2017

Member

IMO this constant (BIP32_NONHARDENED_KEY_LIMIT) is superfluous. Just use 0.

src/wallet/wallet.cpp
+
+ // derive child key at next index, skip keys already known to the wallet
+ do {
+ externalChainChildKey.Derive(childKey, hdChain.nExternalChainCounter | BIP32_NONHARDENED_KEY_LIMIT);
@jonasschnelli

jonasschnelli Mar 17, 2017

Member

Also here, ... just use hdChain.nExternalChainCounter instead of hdChain.nExternalChainCounter | BIP32_NONHARDENED_KEY_LIMIT (no need to bitwise OR it with 0).

src/wallet/wallet.cpp
- CPubKey pubkey = secret.GetPubKey();
- assert(secret.VerifyPubKey(pubkey));
+ if (!IsHDWatchOnly())
+ assert(secret.VerifyPubKey(pubkey));
@jonasschnelli

jonasschnelli Mar 17, 2017

Member

I think there is no way to verify the correctness of the derived pubkey?

src/wallet/walletdb.h
+ if (this->nVersion >= SUPPORT_WATCHONLY_VERSION) {
+ READWRITE(isWatchOnly);
+ if(isWatchOnly)
+ READWRITE(masterPubKey);
@jonasschnelli

jonasschnelli Mar 17, 2017

Member

Somehow this PR was crashing here because of

Assertion failed: (pubkey.size() == 33), function Encode, file pubkey.cpp, line 255.

It looks like that my tested tpub was non compressed? How is that even possible?
LLDB told me pubkey = (vch = unsigned char [65] @ 0x00007fd16736976c).

@jonasschnelli

jonasschnelli Mar 17, 2017

Member

Wait... I was using the extended private key instead. Though, I guess it should not crash in this case.

@NicolasDorier

NicolasDorier Mar 20, 2017

Member

How is it possible to get an extended private key here? if you provided a xpriv instead of xpub, it should have crashed at startup.

@jonasschnelli

jonasschnelli Mar 20, 2017

Member

I used a tpriv which made this PR proceeding the hdwatchonly initialisation but then crashed at this point. Haven't looked into the details why the tpriv was accepted during init.

@NicolasDorier

NicolasDorier Mar 20, 2017

Member

ah OK I know why... I do not verify the prefix

@NicolasDorier

NicolasDorier Mar 20, 2017

Member

Issue fixed and tested

src/wallet/wallet.cpp
+ CExtPubKey extPubKey;
+ if (!hdWatchOnly.empty()) {
+ CBitcoinExtPubKey bitcoinExtPubKey;
+ auto extpubKeyPrefix = chainparams.Base58Prefix(CChainParams::Base58Type::EXT_PUBLIC_KEY);
@jonasschnelli

jonasschnelli Mar 17, 2017

Member

Hmm.. I think this is incorrect. I could load a xpub (mainnet) on regtest.
Shouldn't CBitcoinExtPubKey("<x|tpub>") do the check for the correct encoding according to the chainparams?

src/wallet/wallet.cpp
+ extPubKey = bitcoinExtPubKey.GetKey();
+ }
+
+ if (!fFirstRun && !hdWatchOnly.empty()) {
@jonasschnelli

jonasschnelli Mar 17, 2017

Member

I got no warning when I started bitcoind again with a different -hdwatchonly=xpub.
Here there should be a check wether the wallet has already a watch-only xpub set, and if the user provides one, and it's different, it should warn or stop.

@NicolasDorier

NicolasDorier Mar 20, 2017 edited

Member

this is normally checked. Actually I am testing it on https://github.com/bitcoin/bitcoin/pull/9728/files#diff-08fcd10ff4d7d4f9f0249ba978b3b4d4R49 this is strange... actual check done on https://github.com/bitcoin/bitcoin/pull/9728/files#diff-b2bb174788c7409b671c46ccc86034bdR3731 can you verify that you did not made another mistake ?

Member

jonasschnelli commented Mar 17, 2017

This is a great PR. I really would like this to be in 0.15. It makes HWW signing much simpler and could be a pre-step for HWW support in Core.

Member

NicolasDorier commented Mar 18, 2017

I will rebase and pass on all your nits and better test/code the case where you pass a xpriv once #9294 get merged.

Member

NicolasDorier commented Mar 20, 2017

  1. Removed the addition of CChainParams into InitLoadWallet
  2. Correctly error if not passing an extpubkey with the right network
  3. Testing hdwatchonly parsing

NicolasDorier referenced this pull request Mar 22, 2017

Open

TODO for release notes 0.15.0 #9889

0 of 10 tasks complete

fanquake added this to the 0.15.0 milestone Mar 22, 2017

Member

NicolasDorier commented Apr 7, 2017 edited

Rebased:

NicolasDorier referenced this pull request in NTumbleBit/NTumbleBit May 24, 2017

Closed

Drop the forceful support of BIP44 and make it optional #52

Member

NicolasDorier commented Jun 5, 2017 edited

Rebased @saleemrashid

ACK

Member

instagibbs commented Jun 15, 2017

needs rebase, will review

Member

NicolasDorier commented Jun 16, 2017

rebased

Member

instagibbs commented Jun 18, 2017

Is there a way exposed to the user that their wallet is HD watch-only? Might be useful in getwalletinfo.

Member

NicolasDorier commented Jun 18, 2017

just added info in getwalletinfo. I sadly can't compile because of #10622 and it seems my travis build is queued and does not want to run.

Member

NicolasDorier commented Jun 19, 2017

Ok my blinded push passed @instagibbs . Added some info in getwalletinfo as suggested.

@instagibbs

tACK, works as expected

src/wallet/wallet.cpp
@@ -111,9 +111,7 @@ CPubKey CWallet::GenerateNewKey(bool internal)
SetMinVersion(FEATURE_COMPRPUBKEY);
if (!IsHDWatchOnly())
- {
@instagibbs

instagibbs Jun 19, 2017

Member

Braces are the recommended code style, not sure why they're being removed.

src/wallet/wallet.cpp
}
// Compressed public keys were introduced in version 0.6.0
if (fCompressed)
SetMinVersion(FEATURE_COMPRPUBKEY);
- CPubKey pubkey = secret.GetPubKey();
- assert(secret.VerifyPubKey(pubkey));
+ if (!IsHDWatchOnly())
@instagibbs

instagibbs Jun 19, 2017

Member

Please add braces.

src/wallet/wallet.cpp
- CPubKey pubkey = secret.GetPubKey();
- assert(secret.VerifyPubKey(pubkey));
+ if (!IsHDWatchOnly())
+ assert(secret.VerifyPubKey(pubkey));
mapKeyMetadata[pubkey.GetID()] = metadata;
@instagibbs

instagibbs Jun 19, 2017

Member

Perhaps move this into AddKeyPubKey

src/wallet/wallet.cpp
+ if (!fFirstRun) {
+ if (!walletInstance->IsHDWatchOnly() ||
+ walletInstance->GetHDChain().masterKeyID != extPubKey.pubkey.GetID()) {
+ InitError(_("Cannot specify hdwatchonly on an already existing wallet"));
@instagibbs

instagibbs Jun 19, 2017

Member

Message should be something like "Cannot specify new hdwatchonly on an already existing wallet"

src/wallet/rpcwallet.cpp
@@ -2400,7 +2400,8 @@ UniValue getwalletinfo(const JSONRPCRequest& request)
" \"keypoolsize_hd_internal\": xxxx, (numeric) how many new keys are pre-generated for internal use (used for change outputs, only appears if the wallet is using this feature, otherwise external keys are used)\n"
" \"unlocked_until\": ttt, (numeric) the timestamp in seconds since epoch (midnight Jan 1 1970 GMT) that the wallet is unlocked for transfers, or 0 if the wallet is locked\n"
" \"paytxfee\": x.xxxx, (numeric) the transaction fee configuration, set in " + CURRENCY_UNIT + "/kB\n"
- " \"hdmasterkeyid\": \"<hash160>\" (string) the Hash160 of the HD master pubkey\n"
+ " \"hdmasterkeyid\": \"<hash160>\" (string) the Hash160 of the HD master pubkey\n" +
+ " \"hdwatchonlykey\": \"<hdpubkey>\" (string) the HD pubkey used for key derivation in hdwatchonly mode\n"
@instagibbs

instagibbs Jun 19, 2017

Member

"extended pubkey"

src/wallet/walletdb.cpp
@@ -57,6 +57,14 @@ bool CWalletDB::EraseTx(uint256 hash)
return EraseIC(std::make_pair(std::string("tx"), hash));
}
+bool CWalletDB::WriteKeyMeta(const CPubKey& vchPubKey, const CKeyMetadata& keyMeta)
+{
+ if (!batch.Write(std::make_pair(std::string("keymeta"), vchPubKey),
@instagibbs

instagibbs Jun 19, 2017

Member

Please add braces for readability

Should you be using WriteIC like everywhere else?

test/functional/hdwatchonly.py
+
+ self.stop_nodes()
+
+ # check fails gracefully if any mess up with hdwatchonly parameter
@instagibbs

instagibbs Jun 19, 2017

Member

suggested rewording:

"check for graceful failure due to any invalid hdwatchonly parameters"

test/functional/hdwatchonly.py
+ validated_address = self.nodes[0].validateaddress('mxKeRQP6gTdCW6jHhn9FW8bGXD8W1UpR6n')
+ assert_equal(validated_address['hdkeypath'], 'm/0/1')
+
+ # check the hd key was persisted
@instagibbs

instagibbs Jun 19, 2017

Member

s/was/has/

Member

NicolasDorier commented Jun 19, 2017 edited

Thanks for the review @instagibbs I addressed your nits, can you re tACK once ?

Member

instagibbs commented Jun 19, 2017

re-tACK 067b117

Member

instagibbs commented Jun 19, 2017

We could actually also return the xpub in validateaddress as well...

Member

instagibbs commented Jun 26, 2017 edited

Included the return in validateaddress here: instagibbs/bitcoin@b8e1316

Please feel free to take.

Using this PR "in production" with no issues.

laanwj self-assigned this Jun 27, 2017

+ const auto& meta = pwallet->mapKeyMetadata;
+ auto it = meta.find(CScriptID(parentOut.scriptPubKey));
+ if (it == meta.end() ||
+ it->second.hdKeypath.empty())
@instagibbs

instagibbs Jun 27, 2017 edited

Member

I think we may need to generalize this check a bit more. If the script here is a multisig in which the watch-only wallet owns all the keys, this will return false since there is no hdKeypath.

Practically this means that spending 0-conf p2sh funds is a dicey experience that results in "Insufficient funds" responses sometimes.

Perhaps something like pwalletMain->IsHDWatchOnly(CScriptID& scriptID) to make this check, which accounts for this situation?

@instagibbs

instagibbs Jun 27, 2017

Member

Since we're not even required to be backwards compatible, I think adding a new piece of key metadata fIsHDWatchOnly, and adding that during importaddress(or some new call) when all keys are hdwatchonly keys(or a mixture of watchonly and stored private keys?).

@NicolasDorier

NicolasDorier Jun 28, 2017 edited

Member

Does IsTrusted() returns ISMINE_SPENDABLE when for a multi sig output where we know all the keys when not hdwatchonly?

@instagibbs

instagibbs Jun 28, 2017

Member

If you have the private keys, yes, because of check a few lines above.

@NicolasDorier

NicolasDorier Jun 29, 2017

Member

So your goal is that importaddress scriptPubKeys should be considered trusted if we are in hdwatchonly mode ?

@instagibbs

instagibbs Jun 29, 2017

Member

If we completely those addresses via hdwatchonly keys and/or privkeys, yes.

@instagibbs

instagibbs Jun 29, 2017

Member

Currently in this mode you can import a privkey and spend unconfirmed outputs. Likewise you can spend unconfirmed hdwatchonly outputs. You cannot however spend a mixture of the two in a multisig, or pure hdwatchonly p2sh.

Member

NicolasDorier commented Jun 28, 2017

Rebased and included @instagibbs commit.

Member

instagibbs commented Jun 29, 2017

Not trying to completely sidetrack this PR, but this is for future PR consideration:

@NicolasDorier instagibbs/bitcoin@bb72d12

This is the implementation that works for unconfirmed p2sh multisig where all keys are mixture of imported privkeys and hdwatchonly. Pretty ugly though and quite special-cased. Longer-term I'd look to extend ISMINE instead to have a value which covers this consideration and make this whole IsTrusted logic much simpler.

Member

NicolasDorier commented Jun 30, 2017

I think making more general case with SPENDABLE_WATCHONLY is preferable.
I sadly don't have too much time these days, but I am interested into fixing it during 27-28.

- if (!GetKey(hdChain.masterKeyID, key))
+ if (GetKey(hdChain.masterKeyID, key))
+ {
+ // for now we use a fixed keypath scheme of m/0'/0'/k
@jtimon

jtimon Jul 13, 2017

Member

it seems like you are repeating yourself a little bit here. Perhaps a common function can be extracted to avoid some code duplication between this if and the next one?

@NicolasDorier

NicolasDorier Jul 15, 2017

Member

I tried to think about this, but this was not as easy. I preferred keeping this way as it makes review easier: I did not touch what was already working.

@laanwj laanwj modified the milestone: 0.16.0, 0.15.0 Jul 17, 2017

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