diff --git a/src/base58.h b/src/base58.h index 4de5cc6ce5f..be81bd9dc78 100644 --- a/src/base58.h +++ b/src/base58.h @@ -146,7 +146,8 @@ template class CBitcoinExtK K GetKey() { K ret; - if (vchData.size() == Size) { + const bool fCorrectVersion = vchVersion == Params().Base58Prefix(Type); + if (vchData.size() == Size && fCorrectVersion) { // If base58 encoded data does not hold an ext key, return a !IsValid() key ret.Decode(vchData.data()); } diff --git a/src/keystore.cpp b/src/keystore.cpp index 8454175ca81..f708b735edb 100644 --- a/src/keystore.cpp +++ b/src/keystore.cpp @@ -106,6 +106,16 @@ bool CBasicKeyStore::HaveWatchOnly(const CScript &dest) const return setWatchOnly.count(dest) > 0; } +bool CBasicKeyStore::HaveWatchOnly(const CKeyID &keyId) const +{ + LOCK(cs_KeyStore); + WatchKeyMap::const_iterator it = mapWatchKeys.find(keyId); + if (it != mapWatchKeys.end()) { + return true; + } + return false; +} + bool CBasicKeyStore::HaveWatchOnly() const { LOCK(cs_KeyStore); diff --git a/src/keystore.h b/src/keystore.h index 965ae0c79ad..699819b1e54 100644 --- a/src/keystore.h +++ b/src/keystore.h @@ -104,6 +104,8 @@ class CBasicKeyStore : public CKeyStore virtual bool AddWatchOnly(const CScript &dest) override; virtual bool RemoveWatchOnly(const CScript &dest) override; virtual bool HaveWatchOnly(const CScript &dest) const override; + + virtual bool HaveWatchOnly(const CKeyID &keyId) const; virtual bool HaveWatchOnly() const override; }; diff --git a/src/rpc/misc.cpp b/src/rpc/misc.cpp index f3c86038a3e..2a730c92ea6 100644 --- a/src/rpc/misc.cpp +++ b/src/rpc/misc.cpp @@ -243,6 +243,11 @@ UniValue validateaddress(const JSONRPCRequest& request) ret.push_back(Pair("hdmasterkeyid", it->second.hdMasterKeyID.GetHex())); } } + + if (pwallet->IsExternalHD()) { + CBitcoinExtPubKey externalHD(pwallet->GetHDChain().externalHD); + ret.push_back(Pair("externalhdkey", externalHD.ToString())); + } } #endif } diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index a7c229fa74d..0fa732daddb 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -2510,7 +2510,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\": \"\" (string) the Hash160 of the HD master pubkey\n" + " \"hdmasterkeyid\": \"\" (string) the Hash160 of the HD master pubkey\n" + + " \"externalhdkey\": \"\" (string) the extended pubkey used for key derivation in external HD mode\n" "}\n" "\nExamples:\n" + HelpExampleCli("getwalletinfo", "") @@ -2537,6 +2538,10 @@ UniValue getwalletinfo(const JSONRPCRequest& request) if (pwallet->IsCrypted()) { obj.push_back(Pair("unlocked_until", pwallet->nRelockTime)); } + if (pwallet->IsExternalHD()) { + CBitcoinExtPubKey watchOnly(pwallet->GetHDChain().externalHD); + obj.push_back(Pair("externalhdkey", watchOnly.ToString())); + } obj.push_back(Pair("paytxfee", ValueFromAmount(payTxFee.GetFeePerK()))); if (!masterKeyID.IsNull()) obj.push_back(Pair("hdmasterkeyid", masterKeyID.GetHex())); diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index d236b1de3f5..f4bd6f4d833 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -95,16 +95,18 @@ CPubKey CWallet::GenerateNewKey(CWalletDB &walletdb, bool internal) bool fCompressed = CanSupportFeature(FEATURE_COMPRPUBKEY); // default to compressed public keys if we want 0.6.0 wallets CKey secret; - + CPubKey pubkey; // Create new metadata int64_t nCreationTime = GetTime(); CKeyMetadata metadata(nCreationTime); // use HD key derivation if HD was enabled during wallet creation if (IsHDEnabled()) { - DeriveNewChildKey(walletdb, metadata, secret, (CanSupportFeature(FEATURE_HD_SPLIT) ? internal : false)); + DeriveNewChildKey(walletdb, metadata, secret, pubkey, (CanSupportFeature(FEATURE_HD_SPLIT) ? internal : false)); } else { secret.MakeNewKey(fCompressed); + pubkey = secret.GetPubKey(); + assert(secret.VerifyPubKey(pubkey)); } // Compressed public keys were introduced in version 0.6.0 @@ -112,62 +114,97 @@ CPubKey CWallet::GenerateNewKey(CWalletDB &walletdb, bool internal) SetMinVersion(FEATURE_COMPRPUBKEY); } - CPubKey pubkey = secret.GetPubKey(); - assert(secret.VerifyPubKey(pubkey)); - mapKeyMetadata[pubkey.GetID()] = metadata; UpdateTimeFirstKey(nCreationTime); - if (!AddKeyPubKeyWithDB(walletdb, secret, pubkey)) { - throw std::runtime_error(std::string(__func__) + ": AddKey failed"); + if(!IsExternalHD()) { + if (!AddKeyPubKeyWithDB(walletdb, secret, pubkey)) { + throw std::runtime_error(std::string(__func__) + ": AddKey failed"); + } + } + else { + if (!AddWatchOnly(pubkey, metadata, nCreationTime)) + throw std::runtime_error(std::string(__func__) + ": AddWatchOnly failed"); } return pubkey; } -void CWallet::DeriveNewChildKey(CWalletDB &walletdb, CKeyMetadata& metadata, CKey& secret, bool internal) + +void CWallet::DeriveNewChildKey(CWalletDB &walletdb, CKeyMetadata& metadata, CKey& secret, CPubKey& pubkey, bool internal) { - // for now we use a fixed keypath scheme of m/0'/0'/k CKey key; //master key seed (256bit) - CExtKey masterKey; //hd master key - CExtKey accountKey; //key at m/0' - CExtKey chainChildKey; //key at m/0'/0' (external) or m/0'/1' (internal) - CExtKey childKey; //key at m/0'/0'/' - // try to get the master key - if (!GetKey(hdChain.masterKeyID, key)) + if (GetKey(hdChain.masterKeyID, key)) + { + // for now we use a fixed keypath scheme of m/0'/0'/k + CExtKey masterKey; //hd master key + CExtKey accountKey; //key at m/0' + CExtKey chainChildKey; //key at m/0'/0' (external) or m/0'/1' (internal) + CExtKey childKey; //key at m/0'/0'/' + + masterKey.SetMaster(key.begin(), key.size()); + + // derive m/0' + // use hardened derivation (child keys >= 0x80000000 are hardened after bip32) + masterKey.Derive(accountKey, BIP32_HARDENED_KEY_LIMIT); + + // derive m/0'/0' (external chain) OR m/0'/1' (internal chain) + assert(internal ? CanSupportFeature(FEATURE_HD_SPLIT) : true); + accountKey.Derive(chainChildKey, BIP32_HARDENED_KEY_LIMIT + (internal ? 1 : 0)); + + // derive child key at next index, skip keys already known to the wallet + do { + // always derive hardened keys + // childIndex | BIP32_HARDENED_KEY_LIMIT = derive childIndex in hardened child-index-range + // example: 1 | BIP32_HARDENED_KEY_LIMIT == 0x80000001 == 2147483649 + if (internal) { + chainChildKey.Derive(childKey, hdChain.nInternalChainCounter | BIP32_HARDENED_KEY_LIMIT); + metadata.hdKeypath = "m/0'/1'/" + std::to_string(hdChain.nInternalChainCounter) + "'"; + hdChain.nInternalChainCounter++; + } + else { + chainChildKey.Derive(childKey, hdChain.nExternalChainCounter | BIP32_HARDENED_KEY_LIMIT); + metadata.hdKeypath = "m/0'/0'/" + std::to_string(hdChain.nExternalChainCounter) + "'"; + hdChain.nExternalChainCounter++; + } + } while (HaveKey(childKey.key.GetPubKey().GetID())); + secret = childKey.key; + pubkey = childKey.key.GetPubKey(); + metadata.hdMasterKeyID = hdChain.masterKeyID; + // update the chain model in the database + if (!walletdb.WriteHDChain(hdChain)) + throw std::runtime_error(std::string(__func__) + ": Writing HD chain model failed"); + } + else if (IsExternalHD()) { + CExtPubKey& masterKey = hdChain.externalHD; //hd master key + CExtPubKey chainChildKey; //key at m/0 (external) or m/1 (internal) + CExtPubKey childKey; //key at m/0/ + + // derive m/x + masterKey.Derive(chainChildKey, internal ? 1 : 0); + + // derive child key at next index, skip keys already known to the wallet + do { + if (internal) { + chainChildKey.Derive(childKey, hdChain.nInternalChainCounter); + metadata.hdKeypath = "m/1/" + std::to_string(hdChain.nInternalChainCounter); + hdChain.nInternalChainCounter++; + } + else { + chainChildKey.Derive(childKey, hdChain.nExternalChainCounter); + metadata.hdKeypath = "m/0/" + std::to_string(hdChain.nExternalChainCounter); + hdChain.nExternalChainCounter++; + } + metadata.hdMasterKeyID = hdChain.masterKeyID; + } while (HaveWatchOnly(childKey.pubkey.GetID())); + pubkey = childKey.pubkey; + // update the chain model in the database + if (!walletdb.WriteHDChain(hdChain)) + throw std::runtime_error(std::string(__func__) + ": Writing HD chain model failed"); + } + else { throw std::runtime_error(std::string(__func__) + ": Master key not found"); - - masterKey.SetMaster(key.begin(), key.size()); - - // derive m/0' - // use hardened derivation (child keys >= 0x80000000 are hardened after bip32) - masterKey.Derive(accountKey, BIP32_HARDENED_KEY_LIMIT); - - // derive m/0'/0' (external chain) OR m/0'/1' (internal chain) - assert(internal ? CanSupportFeature(FEATURE_HD_SPLIT) : true); - accountKey.Derive(chainChildKey, BIP32_HARDENED_KEY_LIMIT+(internal ? 1 : 0)); - - // derive child key at next index, skip keys already known to the wallet - do { - // always derive hardened keys - // childIndex | BIP32_HARDENED_KEY_LIMIT = derive childIndex in hardened child-index-range - // example: 1 | BIP32_HARDENED_KEY_LIMIT == 0x80000001 == 2147483649 - if (internal) { - chainChildKey.Derive(childKey, hdChain.nInternalChainCounter | BIP32_HARDENED_KEY_LIMIT); - metadata.hdKeypath = "m/0'/1'/" + std::to_string(hdChain.nInternalChainCounter) + "'"; - hdChain.nInternalChainCounter++; - } - else { - chainChildKey.Derive(childKey, hdChain.nExternalChainCounter | BIP32_HARDENED_KEY_LIMIT); - metadata.hdKeypath = "m/0'/0'/" + std::to_string(hdChain.nExternalChainCounter) + "'"; - hdChain.nExternalChainCounter++; - } - } while (HaveKey(childKey.key.GetPubKey().GetID())); - secret = childKey.key; - metadata.hdMasterKeyID = hdChain.masterKeyID; - // update the chain model in the database - if (!walletdb.WriteHDChain(hdChain)) - throw std::runtime_error(std::string(__func__) + ": Writing HD chain model failed"); + } } bool CWallet::AddKeyPubKeyWithDB(CWalletDB &walletdb, const CKey& secret, const CPubKey &pubkey) @@ -298,6 +335,29 @@ bool CWallet::AddWatchOnly(const CScript& dest, int64_t nCreateTime) return AddWatchOnly(dest); } +bool CWallet::AddWatchOnly(const CPubKey &pubkey, const CKeyMetadata& meta, int64_t nCreateTime) +{ + auto script = GetScriptForDestination(pubkey.GetID()); + if (!HaveWatchOnly(script)) + { + mapKeyMetadata[CScriptID(script)] = meta; + if (!AddWatchOnly(script, nCreateTime)) + return false; + } + + if (!CWalletDB(*dbw).WriteKeyMeta(pubkey, meta)) + return false; + + script = GetScriptForRawPubKey(pubkey); + if (!HaveWatchOnly(script)) + { + mapKeyMetadata[CScriptID(script)] = meta; + if (!AddWatchOnly(script, nCreateTime)) + return false; + } + return true; +} + bool CWallet::RemoveWatchOnly(const CScript &dest) { AssertLockHeld(cs_wallet); @@ -1393,6 +1453,25 @@ bool CWallet::SetHDMasterKey(const CPubKey& pubkey) return true; } +bool CWallet::SetExternalHD(const CExtPubKey& extPubKey) +{ + LOCK(cs_wallet); + + // ensure this wallet.dat can only be opened by clients supporting HD + SetMinVersion(FEATURE_EXTERNAL_HD); + + // store the keyid (hash160) together with + // the child index counter in the database + // as a hdchain object + CHDChain newHdChain; + newHdChain.masterKeyID = extPubKey.pubkey.GetID(); + newHdChain.isExternalHD = true; + newHdChain.externalHD = extPubKey; + SetHDChain(newHdChain, false); + + return true; +} + bool CWallet::SetHDChain(const CHDChain& chain, bool memonly) { LOCK(cs_wallet); @@ -1408,6 +1487,11 @@ bool CWallet::IsHDEnabled() const return !hdChain.masterKeyID.IsNull(); } +bool CWallet::IsExternalHD() const +{ + return IsHDEnabled() && hdChain.isExternalHD; +} + int64_t CWalletTx::GetTxTime() const { int64_t n = nTimeSmart; @@ -1848,8 +1932,18 @@ bool CWalletTx::IsTrusted() const if (parent == NULL) return false; const CTxOut& parentOut = parent->tx->vout[txin.prevout.n]; - if (pwallet->IsMine(parentOut) != ISMINE_SPENDABLE) - return false; + const auto& isMine = pwallet->IsMine(parentOut); + if (isMine != ISMINE_SPENDABLE) + { + // If the wallet is external HD, check if it is a key we generated + if (!pwallet->IsExternalHD() || isMine != ISMINE_WATCH_SOLVABLE) + return false; + const auto& meta = pwallet->mapKeyMetadata; + auto it = meta.find(CScriptID(parentOut.scriptPubKey)); + if (it == meta.end() || + it->second.hdKeypath.empty()) + return false; + } } return true; } @@ -3290,9 +3384,14 @@ void CWallet::ReserveKeyFromKeyPool(int64_t& nIndex, CKeyPool& keypool, bool fRe if (!walletdb.ReadPool(nIndex, keypool)) { throw std::runtime_error(std::string(__func__) + ": read failed"); } - if (!HaveKey(keypool.vchPubKey.GetID())) { + if (!IsExternalHD() && !HaveKey(keypool.vchPubKey.GetID())) { throw std::runtime_error(std::string(__func__) + ": unknown key in key pool"); } + if (IsExternalHD()) { + if (!HaveWatchOnly(keypool.vchPubKey.GetID())) { + throw std::runtime_error(std::string(__func__) + ": unknown key in key pool"); + } + } if (keypool.fInternal != fReturningInternal) { throw std::runtime_error(std::string(__func__) + ": keypool entry misclassified"); } @@ -3855,6 +3954,7 @@ std::string CWallet::GetWalletHelpString(bool showDebug) strUsage += HelpMessageOpt("-spendzeroconfchange", strprintf(_("Spend unconfirmed change when sending transactions (default: %u)"), DEFAULT_SPEND_ZEROCONF_CHANGE)); strUsage += HelpMessageOpt("-txconfirmtarget=", strprintf(_("If paytxfee is not set, include enough fee so transactions begin confirmation on average within n blocks (default: %u)"), DEFAULT_TX_CONFIRM_TARGET)); strUsage += HelpMessageOpt("-usehd", _("Use hierarchical deterministic key generation (HD) after BIP32. Only has effect during wallet creation/first start") + " " + strprintf(_("(default: %u)"), DEFAULT_USE_HD_WALLET)); + strUsage += HelpMessageOpt("-externalhd", _("Create a new external-HD wallet from a BIP32 HD public key")); strUsage += HelpMessageOpt("-walletrbf", strprintf(_("Send transactions with full-RBF opt-in enabled (default: %u)"), DEFAULT_WALLET_RBF)); strUsage += HelpMessageOpt("-upgradewallet", _("Upgrade wallet to latest format on startup")); strUsage += HelpMessageOpt("-wallet=", _("Specify wallet file (within data directory)") + " " + strprintf(_("(default: %s)"), DEFAULT_WALLET_DAT)); @@ -3949,6 +4049,26 @@ CWallet* CWallet::CreateWalletFromFile(const std::string walletFile) walletInstance->SetMaxVersion(nMaxVersion); } + + std::string externalHd = GetArg("-externalhd", ""); + CExtPubKey extPubKey; + if (!externalHd.empty()) { + CBitcoinExtPubKey bitcoinExtPubKey(externalHd); + extPubKey = bitcoinExtPubKey.GetKey(); + if (!extPubKey.pubkey.IsFullyValid()) { + InitError(_("Invalid ExtPubKey format")); + return NULL; + } + + if (!fFirstRun) { + if (!walletInstance->IsExternalHD() || + walletInstance->GetHDChain().masterKeyID != extPubKey.pubkey.GetID()) { + InitError(_("Cannot specify new external hd on an already existing wallet")); + return NULL; + } + } + } + if (fFirstRun) { // Create new keyUser and set as default key @@ -3958,9 +4078,15 @@ CWallet* CWallet::CreateWalletFromFile(const std::string walletFile) walletInstance->SetMinVersion(FEATURE_HD_SPLIT); // generate a new master key - CPubKey masterPubKey = walletInstance->GenerateNewHDMasterKey(); - if (!walletInstance->SetHDMasterKey(masterPubKey)) - throw std::runtime_error(std::string(__func__) + ": Storing master key failed"); + if(externalHd.empty()) { + CPubKey masterPubKey = walletInstance->GenerateNewHDMasterKey(); + if (!walletInstance->SetHDMasterKey(masterPubKey)) + throw std::runtime_error(std::string(__func__) + ": Storing master key failed"); + } + else { + if (!walletInstance->SetExternalHD(extPubKey)) + throw std::runtime_error(std::string(__func__) + ": Storing master key failed"); + } } CPubKey newDefaultKey; if (walletInstance->GetKeyFromPool(newDefaultKey, false)) { diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index 7ef2e6f1d8e..852e4832210 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -80,6 +80,7 @@ class CScript; class CScheduler; class CTxMemPool; class CBlockPolicyEstimator; +class CChainParams; class CWalletTx; struct FeeCalculation; enum class FeeEstimateMode; @@ -95,6 +96,7 @@ enum WalletFeature FEATURE_HD = 130000, // Hierarchical key derivation after BIP32 (HD Wallet) FEATURE_HD_SPLIT = 139900, // Wallet with HD chain split (change outputs will use m/0'/1'/k) + FEATURE_EXTERNAL_HD = 139901, // External Hierarchical key derivation after BIP32 (HD Wallet) FEATURE_LATEST = FEATURE_COMPRPUBKEY // HD is optional, use FEATURE_COMPRPUBKEY as latest version }; @@ -699,7 +701,7 @@ class CWallet : public CCryptoKeyStore, public CValidationInterface CHDChain hdChain; /* HD derive new child key (on internal or external chain) */ - void DeriveNewChildKey(CWalletDB &walletdb, CKeyMetadata& metadata, CKey& secret, bool internal = false); + void DeriveNewChildKey(CWalletDB &walletdb, CKeyMetadata& metadata, CKey& secret, CPubKey& pubkey, bool internal = false); std::set setInternalKeyPool; std::set setExternalKeyPool; @@ -903,6 +905,8 @@ class CWallet : public CCryptoKeyStore, public CValidationInterface //! Adds a watch-only address to the store, and saves it to disk. bool AddWatchOnly(const CScript& dest, int64_t nCreateTime); + bool AddWatchOnly(const CPubKey &pubkey, const CKeyMetadata& meta, int64_t nCreateTime); + bool RemoveWatchOnly(const CScript &dest) override; //! Adds a watch-only address to the store, without saving it to disk (used by LoadWallet) bool LoadWatchOnly(const CScript &dest); @@ -1131,15 +1135,18 @@ class CWallet : public CCryptoKeyStore, public CValidationInterface /* Returns true if HD is enabled */ bool IsHDEnabled() const; + /* Returns true if HD is enabled and is watch only */ + bool IsExternalHD() const; /* Generates a new HD master key (will not be activated) */ CPubKey GenerateNewHDMasterKey(); - + /* Set the current HD master key (will reset the chain child index counters) - Sets the master key's version based on the current wallet version (so the - caller must ensure the current wallet version is correct before calling - this function). */ + Sets the master key's version based on the current wallet version (so the + caller must ensure the current wallet version is correct before calling + this function). */ bool SetHDMasterKey(const CPubKey& key); + bool SetExternalHD(const CExtPubKey& extPubKey); }; /** A key allocated from the key pool. */ diff --git a/src/wallet/walletdb.cpp b/src/wallet/walletdb.cpp index 65a28af46dc..04bd921669d 100644 --- a/src/wallet/walletdb.cpp +++ b/src/wallet/walletdb.cpp @@ -56,6 +56,12 @@ bool CWalletDB::EraseTx(uint256 hash) return EraseIC(std::make_pair(std::string("tx"), hash)); } +bool CWalletDB::WriteKeyMeta(const CPubKey& vchPubKey, const CKeyMetadata& keyMeta) +{ + return WriteIC(std::make_pair(std::string("keymeta"), vchPubKey), + keyMeta, false); +} + bool CWalletDB::WriteKey(const CPubKey& vchPubKey, const CPrivKey& vchPrivKey, const CKeyMetadata& keyMeta) { if (!WriteIC(std::make_pair(std::string("keymeta"), vchPubKey), keyMeta, false)) { diff --git a/src/wallet/walletdb.h b/src/wallet/walletdb.h index d78f143ebd6..4aee597cb25 100644 --- a/src/wallet/walletdb.h +++ b/src/wallet/walletdb.h @@ -63,10 +63,14 @@ class CHDChain uint32_t nExternalChainCounter; uint32_t nInternalChainCounter; CKeyID masterKeyID; //!< master key hash160 + CExtPubKey externalHD; + bool isExternalHD = false; static const int VERSION_HD_BASE = 1; static const int VERSION_HD_CHAIN_SPLIT = 2; - static const int CURRENT_VERSION = VERSION_HD_CHAIN_SPLIT; + static const int SUPPORT_EXTERNALHD_VERSION = 3; + static const int CURRENT_VERSION = SUPPORT_EXTERNALHD_VERSION; + int nVersion; CHDChain() { SetNull(); } @@ -79,6 +83,11 @@ class CHDChain READWRITE(masterKeyID); if (this->nVersion >= VERSION_HD_CHAIN_SPLIT) READWRITE(nInternalChainCounter); + if (this->nVersion >= SUPPORT_EXTERNALHD_VERSION) { + READWRITE(isExternalHD); + if(isExternalHD) + READWRITE(externalHD); + } } void SetNull() @@ -177,6 +186,7 @@ class CWalletDB bool WriteTx(const CWalletTx& wtx); bool EraseTx(uint256 hash); + bool WriteKeyMeta(const CPubKey& vchPubKey, const CKeyMetadata& keyMeta); bool WriteKey(const CPubKey& vchPubKey, const CPrivKey& vchPrivKey, const CKeyMetadata &keyMeta); bool WriteCryptedKey(const CPubKey& vchPubKey, const std::vector& vchCryptedSecret, const CKeyMetadata &keyMeta); bool WriteMasterKey(unsigned int nID, const CMasterKey& kMasterKey); diff --git a/test/functional/externalhd.py b/test/functional/externalhd.py new file mode 100755 index 00000000000..11115c97fb2 --- /dev/null +++ b/test/functional/externalhd.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 +# 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. + +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import * + +class ExternalHDTest(BitcoinTestFramework): + + def __init__(self): + super().__init__() + self.setup_clean_chain = True + self.num_nodes = 2 + + def setup_network(self, split=False): + self.nodes = self.start_nodes(self.num_nodes, self.options.tmpdir, [['-externalhd=tpubD6NzVbkrYhZ4YMc8VtVEChv2fv6eB5RK8ZKmn52hFuqmbGwauf1NjuzzscFzikw7sa41mdE46d9w274Gw29WuFkeVw2VESXxgcMwBQ6NNSf'],[]]) + self.is_network_split=False + + def run_test(self): + print("Mining blocks...") + + # tprv8ZgxMBicQKsPetaLcEpdoJFv6tai1kEQZFizVYzPqe3NkngpHGBnZRP8hUVrrxgaXgckrr2V38HKMTzPMGG5cJq6RymQ1Bn8v9ACJgh9RvG + + # Can generate change address + address = self.nodes[0].getrawchangeaddress() + assert_equal(address, 'mvumWx631FFTDtgWP55ph623xvUfp5Y1xz') + validated_address = self.nodes[0].validateaddress(address) + assert_equal(validated_address['hdkeypath'], 'm/1/0') + address = self.nodes[0].getrawchangeaddress() + assert_equal(address, 'mkrg25GL23RAdnhP6Ttxtu7DzgknmZz3yc') + validated_address = self.nodes[0].validateaddress(address) + assert_equal(validated_address['hdkeypath'], 'm/1/1') + + # Check if getwalletinfo show the external hd pubkey info + assert_equal(self.nodes[0].getwalletinfo()["externalhdkey"], 'tpubD6NzVbkrYhZ4YMc8VtVEChv2fv6eB5RK8ZKmn52hFuqmbGwauf1NjuzzscFzikw7sa41mdE46d9w274Gw29WuFkeVw2VESXxgcMwBQ6NNSf') + assert_equal(self.nodes[0].getwalletinfo()["externalhdkey"], validated_address["externalhdkey"]) + + # Can generate new address (m/0/0 is generated by default at wallet creation) + address = self.nodes[0].getnewaddress() + assert_equal(address, 'mxKeRQP6gTdCW6jHhn9FW8bGXD8W1UpR6n') + validated_address = self.nodes[0].validateaddress(address) + assert_equal(validated_address['hdkeypath'], 'm/0/1') + + self.nodes[0].generatetoaddress(1, address) + self.nodes[0].generate(101) + + unspent = self.nodes[0].listunspent() + assert_equal(len(unspent), 2) + + # generatetoaddress with p2pkh + assert_equal(unspent[0]['solvable'], True) + + # generate mine to p2pk, so let's just be sure we can solve it + assert_equal(unspent[1]['solvable'], True) + + self.stop_nodes() + + # check for graceful failure due to any invalid external hd parameters + self.assert_start_raises_init_error(0, self.options.tmpdir, ['-externalhd=eopipwd'], + 'Invalid ExtPubKey format') + self.assert_start_raises_init_error(0, self.options.tmpdir, ['-externalhd=tprv8ZgxMBicQKsPetaLcEpdoJFv6tai1kEQZFizVYzPqe3NkngpHGBnZRP8hUVrrxgaXgckrr2V38HKMTzPMGG5cJq6RymQ1Bn8v9ACJgh9RvG'], + 'Invalid ExtPubKey format') + self.assert_start_raises_init_error(0, self.options.tmpdir, ['-externalhd=xpubD6NzVbkrYhZ4YTNYPw3XmSoBRZWmfn8mRerv3SEaC8UFiz5geKgCJH42cp9KUzRcfQNSuCQgdM1grUH7FgWYahWKDST3E9NYJMBwMKooTaY'], + 'Invalid ExtPubKey format') + + # should restart fine if external hd is the same as current wallet + self.nodes = self.start_nodes(self.num_nodes, self.options.tmpdir, [['-externalhd=tpubD6NzVbkrYhZ4YMc8VtVEChv2fv6eB5RK8ZKmn52hFuqmbGwauf1NjuzzscFzikw7sa41mdE46d9w274Gw29WuFkeVw2VESXxgcMwBQ6NNSf'],[]]) + + self.stop_nodes() + # should not restart if external hd is different from the current one + self.assert_start_raises_init_error(0, self.options.tmpdir, ['-externalhd=tpubD6NzVbkrYhZ4YTNYPw3XmSoBRZWmfn8mRerv3SEaC8UFiz5geKgCJH42cp9KUzRcfQNSuCQgdM1grUH7FgWYahWKDST3E9NYJMBwMKooTaY'], + 'Cannot specify new external hd on an already existing wallet') + + # check the hdkeypath has persisted + self.nodes = self.start_nodes(self.num_nodes, self.options.tmpdir, [[],[]]) + validated_address = self.nodes[0].validateaddress('mxKeRQP6gTdCW6jHhn9FW8bGXD8W1UpR6n') + assert_equal(validated_address['hdkeypath'], 'm/0/1') + + # check the hd key has persisted + address = self.nodes[0].getnewaddress() + assert_equal(address, 'moZamE3ykhxM5kuBNfnDLnH3iAGd5f8gS5') + validated_address = self.nodes[0].validateaddress(address) + assert_equal(validated_address['hdkeypath'], 'm/0/3') + + # check that scriptPubKey generated by external hd are safe + self.stop_nodes() + self.nodes = self.start_nodes(2, self.options.tmpdir, [[],[]]) + unspent = self.nodes[0].listunspent() + assert_equal(len(unspent), 2) + connect_nodes(self.nodes[1], 0) + sync_chain([self.nodes[0], self.nodes[1]]) + # using private key of mxKeRQP6gTdCW6jHhn9FW8bGXD8W1UpR6n + self.nodes[1].importprivkey("cTNoggeWzJPVK2EQtLb3Yj1J4sxH8Ktx81X9NvxUFwBv1RoPrxUA") + self.nodes[1].sendtoaddress("moZamE3ykhxM5kuBNfnDLnH3iAGd5f8gS5", "0.1") + sync_mempools([self.nodes[0], self.nodes[1]]) + unspent = self.nodes[0].listunspent(0) + # The unconfirmed transaction should be safe + safe_unconf_found = False + for utxo in unspent: + safe_unconf_found |= (utxo["confirmations"] == 0 and utxo["safe"]) + assert(safe_unconf_found) + +if __name__ == '__main__': + ExternalHDTest().main() diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index a043560ea81..d63402a3956 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -116,6 +116,7 @@ 'p2p-leaktests.py', 'wallet-encryption.py', 'uptime.py', + 'externalhd.py' ] EXTENDED_SCRIPTS = [