diff --git a/doc/release-notes-6835.md b/doc/release-notes-6835.md new file mode 100644 index 000000000000..294485a7570a --- /dev/null +++ b/doc/release-notes-6835.md @@ -0,0 +1,21 @@ +Mobile CoinJoin Compatibility +------------ + +- Fixed an issue where CoinJoin funds mixed in Dash Android wallet were + invisible when importing the mnemonic into Dash Core. Descriptor Wallets now + include an additional default descriptor for mobile CoinJoin funds, ensuring + seamless wallet migration and complete fund visibility across different + Dash wallet implementations. + +- This is a breaking change that increases the default number of descriptors + from 2 to 3 on mainnet (internal, external, mobile CoinJoin) for newly created + descriptor wallets only - existing wallets are unaffected. + + +Updated RPCs +------------ + +- The `listdescriptors` RPC now includes an optional coinjoin field to identify + CoinJoin descriptors. + +(#6835) diff --git a/src/wallet/hdchain.cpp b/src/wallet/hdchain.cpp index cc7fc0f76924..3f50b1415e45 100644 --- a/src/wallet/hdchain.cpp +++ b/src/wallet/hdchain.cpp @@ -203,6 +203,7 @@ size_t CHDChain::CountAccounts() std::string CHDPubKey::GetKeyPath() const { - return strprintf("m/44'/%d'/%d'/%d/%d", Params().ExtCoinType(), nAccountIndex, nChangeIndex, extPubKey.nChild); + return strprintf("m/%d'/%d'/%d'/%d/%d", BIP32_PURPOSE_STANDARD, Params().ExtCoinType(), nAccountIndex, nChangeIndex, + extPubKey.nChild); } } // namespace wallet diff --git a/src/wallet/hdchain.h b/src/wallet/hdchain.h index 28385e3d2fb9..3a1d21b36200 100644 --- a/src/wallet/hdchain.h +++ b/src/wallet/hdchain.h @@ -140,6 +140,12 @@ class CHDPubKey std::string GetKeyPath() const; }; + +/** Purpose code used for DIP9 (feature derivation paths) */ +constexpr uint8_t BIP32_PURPOSE_FEATURE{9}; +/** Purpose code allotted to BIP 44 (standard derivation paths) */ +constexpr uint8_t BIP32_PURPOSE_STANDARD{44}; + } // namespace wallet #endif // BITCOIN_WALLET_HDCHAIN_H diff --git a/src/wallet/rpc/backup.cpp b/src/wallet/rpc/backup.cpp index 260b872c4c61..1e3cf53c5293 100644 --- a/src/wallet/rpc/backup.cpp +++ b/src/wallet/rpc/backup.cpp @@ -4,6 +4,7 @@ // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include +#include #include #include #include @@ -1976,6 +1977,7 @@ RPCHelpMan listdescriptors() {RPCResult::Type::NUM, "timestamp", "The creation time of the descriptor"}, {RPCResult::Type::BOOL, "active", "Whether this descriptor is currently used to generate new addresses"}, {RPCResult::Type::BOOL, "internal", /*optional=*/true, "True if this descriptor is used to generate change addresses. False if this descriptor is used to generate receiving addresses; defined only for active descriptors"}, + {RPCResult::Type::BOOL, "coinjoin", /*optional=*/true, "True if this descriptor is used to generate CoinJoin addresses; defined only if it is True."}, {RPCResult::Type::ARR_FIXED, "range", /*optional=*/true, "Defined only for ranged descriptors", { {RPCResult::Type::NUM, "", "Range start inclusive"}, {RPCResult::Type::NUM, "", "Range end inclusive"}, @@ -2036,6 +2038,13 @@ RPCHelpMan listdescriptors() if (active && type != std::nullopt) { spk.pushKV("internal", wallet->GetScriptPubKeyMan(true) == desc_spk_man); } + if (type != std::nullopt) { + std::string match = strprintf("/%d'/%s'/4'/0'", BIP32_PURPOSE_FEATURE, Params().ExtCoinType()); + bool is_cj = descriptor.find(match) != std::string::npos; + if (is_cj) { + spk.pushKV("coinjoin", is_cj); + } + } if (wallet_descriptor.descriptor->IsRange()) { UniValue range(UniValue::VARR); range.push_back(wallet_descriptor.range_start); diff --git a/src/wallet/rpc/wallet.cpp b/src/wallet/rpc/wallet.cpp index 61cdabfa70d3..d7fdae8cd609 100644 --- a/src/wallet/rpc/wallet.cpp +++ b/src/wallet/rpc/wallet.cpp @@ -168,7 +168,7 @@ static RPCHelpMan getwalletinfo() {RPCResult::Type::NUM_TIME, "timefirstkey", "the " + UNIX_EPOCH_TIME + " of the oldest known key in the wallet"}, {RPCResult::Type::NUM_TIME, "keypoololdest", /*optional=*/true, "the " + UNIX_EPOCH_TIME + " of the oldest pre-generated key in the key pool. Legacy wallets only"}, {RPCResult::Type::NUM, "keypoolsize", "how many new keys are pre-generated (only counts external keys)"}, - {RPCResult::Type::NUM, "keypoolsize_hd_internal", /*optional=*/true, "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)"}, + {RPCResult::Type::NUM, "keypoolsize_hd_internal", /*optional=*/ true, "how many new keys are pre-generated for internal use (used for change outputs and mobile coinjoin, only appears if the wallet is using this feature, otherwise external keys are used)"}, {RPCResult::Type::NUM, "keys_left", "how many new keys are left since last automatic backup"}, {RPCResult::Type::NUM_TIME, "unlocked_until", /*optional=*/true, "the " + UNIX_EPOCH_TIME + " until which the wallet is unlocked for transfers, or 0 if the wallet is locked (only present for passphrase-encrypted wallets)"}, {RPCResult::Type::STR_AMOUNT, "paytxfee", "the transaction fee configuration, set in " + CURRENCY_UNIT + "/kB"}, diff --git a/src/wallet/scriptpubkeyman.cpp b/src/wallet/scriptpubkeyman.cpp index 3f685a30df7b..28c354dd6997 100644 --- a/src/wallet/scriptpubkeyman.cpp +++ b/src/wallet/scriptpubkeyman.cpp @@ -2074,7 +2074,7 @@ bool DescriptorScriptPubKeyMan::AddDescriptorKeyWithDB(WalletBatch& batch, const } } -bool DescriptorScriptPubKeyMan::SetupDescriptorGeneration(const CExtKey& master_key, const SecureString& secure_mnemonic, const SecureString& secure_mnemonic_passphrase, bool internal) +bool DescriptorScriptPubKeyMan::SetupDescriptorGeneration(const CExtKey& master_key, const SecureString& secure_mnemonic, const SecureString& secure_mnemonic_passphrase, PathDerivationType type) { LOCK(cs_desc_man); assert(m_storage.IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS)); @@ -2099,10 +2099,12 @@ bool DescriptorScriptPubKeyMan::SetupDescriptorGeneration(const CExtKey& master_ std::string xpub = EncodeExtPubKey(master_key.Neuter()); // Build descriptor string - std::string desc_prefix = strprintf("pkh(%s/44'/%d'", xpub, Params().ExtCoinType()); + std::string desc_prefix = strprintf("pkh(%s/%d'/%d'", xpub, type == PathDerivationType::DIP0009_CoinJoin ? BIP32_PURPOSE_FEATURE : BIP32_PURPOSE_STANDARD, Params().ExtCoinType()); + if (type == PathDerivationType::DIP0009_CoinJoin) { + desc_prefix += "/4'"; + } std::string desc_suffix = "/*)"; - - std::string internal_path = internal ? "/1" : "/0"; + std::string internal_path = (type == PathDerivationType::BIP44_Internal) ? "/1" : "/0"; std::string desc_str = desc_prefix + "/0'" + internal_path + desc_suffix; // Make the descriptor diff --git a/src/wallet/scriptpubkeyman.h b/src/wallet/scriptpubkeyman.h index 12ff6590c8f9..7f4ee75c413b 100644 --- a/src/wallet/scriptpubkeyman.h +++ b/src/wallet/scriptpubkeyman.h @@ -147,6 +147,13 @@ class CKeyPool } }; +enum class PathDerivationType +{ + BIP44_External, + BIP44_Internal, + DIP0009_CoinJoin, +}; + /* * A class implementing ScriptPubKeyMan manages some (or all) scriptPubKeys used in a wallet. * It contains the scripts and keys related to the scriptPubKeys it manages. @@ -575,7 +582,7 @@ class DescriptorScriptPubKeyMan : public ScriptPubKeyMan bool IsHDEnabled() const override; //! Setup descriptors based on the given CExtkey - bool SetupDescriptorGeneration(const CExtKey& master_key, const SecureString& secure_mnemonic, const SecureString& secure_mnemonic_passphrase, bool internal); + bool SetupDescriptorGeneration(const CExtKey& master_key, const SecureString& secure_mnemonic, const SecureString& secure_mnemonic_passphrase, PathDerivationType type); bool HavePrivateKeys() const override; diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index f5b38c98d579..e9c09e8186e0 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -3787,7 +3787,7 @@ void CWallet::SetupDescriptorScriptPubKeyMans(const SecureString& mnemonic_arg, CExtKey master_key; master_key.SetSeed(MakeByteSpan(seed_key)); - for (bool internal : {false, true}) { + for (auto type : {PathDerivationType::BIP44_External, PathDerivationType::BIP44_Internal, PathDerivationType::DIP0009_CoinJoin}) { { // OUTPUT_TYPE is only one: LEGACY auto spk_manager = std::unique_ptr(new DescriptorScriptPubKeyMan(*this)); if (IsCrypted()) { @@ -3798,10 +3798,12 @@ void CWallet::SetupDescriptorScriptPubKeyMans(const SecureString& mnemonic_arg, throw std::runtime_error(std::string(__func__) + ": Could not encrypt new descriptors"); } } - spk_manager->SetupDescriptorGeneration(master_key, mnemonic, mnemonic_passphrase, internal); + spk_manager->SetupDescriptorGeneration(master_key, mnemonic, mnemonic_passphrase, type); uint256 id = spk_manager->GetID(); m_spk_managers[id] = std::move(spk_manager); - AddActiveScriptPubKeyMan(id, internal); + if (type != PathDerivationType::DIP0009_CoinJoin) { + AddActiveScriptPubKeyMan(id, type == PathDerivationType::BIP44_Internal); + } } } } @@ -3826,7 +3828,6 @@ void CWallet::LoadActiveScriptPubKeyMan(uint256 id, bool internal) auto& spk_mans_other = internal ? m_external_spk_managers : m_internal_spk_managers; auto spk_man = m_spk_managers.at(id).get(); spk_mans = spk_man; - if (spk_mans_other == spk_man) { spk_mans_other = nullptr; } diff --git a/test/functional/wallet_listdescriptors.py b/test/functional/wallet_listdescriptors.py index 28de6b2e313b..bd0c18d18e64 100755 --- a/test/functional/wallet_listdescriptors.py +++ b/test/functional/wallet_listdescriptors.py @@ -46,9 +46,11 @@ def run_test(self): node.createwallet(wallet_name='w3', descriptors=True) result = node.get_wallet_rpc('w3').listdescriptors() assert_equal("w3", result['wallet_name']) - assert_equal(2, len(result['descriptors'])) + assert_equal(3, len(result['descriptors'])) assert_equal(2, len([d for d in result['descriptors'] if d['active']])) - assert_equal(1, len([d for d in result['descriptors'] if d['internal']])) + self.log.info(f"result: {result['descriptors']}") + assert_equal(1, len([d for d in result['descriptors'] if 'internal' in d and d['internal']])) + assert_equal(1, len([d for d in result['descriptors'] if 'coinjoin' in d and d['coinjoin']])) for item in result['descriptors']: assert item['desc'] != '' assert item['next_index'] == 0 diff --git a/test/functional/wallet_mnemonicbits.py b/test/functional/wallet_mnemonicbits.py index 6a9574f68327..744fee87b8c6 100755 --- a/test/functional/wallet_mnemonicbits.py +++ b/test/functional/wallet_mnemonicbits.py @@ -47,13 +47,14 @@ def run_test(self): assert_equal(len(desc['mnemonic'].split()), 12) mnemonic_count += 1 assert desc['mnemonic'] == mnemonic_pre - assert desc['active'] + assert_equal(desc['active'], ("coinjoin" not in desc or not desc['coinjoin'])) + # there should 3 descriptors in total # One of them is inactive imported private key for coinbase. It has no mnemonic # Two other should be active and have mnemonic - assert_equal(mnemonic_count, 2) + assert_equal(mnemonic_count, 3) assert_equal(cb_count, 1) - assert_equal(len(descriptors), 3) + assert_equal(len(descriptors), 4) else: assert_equal(len(self.nodes[0].dumphdinfo()["mnemonic"].split()), 12) # 12 words by default # legacy HD wallets could have only one chain