From c62bea41659b56d365a8e8ea7040af141aba0aa0 Mon Sep 17 00:00:00 2001 From: Konstantin Akimov Date: Tue, 26 Aug 2025 02:46:20 +0700 Subject: [PATCH 1/9] feat: create extra spk by default for mobile derivation path of CJ --- src/wallet/rpc/backup.cpp | 10 ++++++++++ src/wallet/rpc/wallet.cpp | 2 +- src/wallet/scriptpubkeyman.cpp | 6 +++--- src/wallet/scriptpubkeyman.h | 9 ++++++++- src/wallet/wallet.cpp | 7 ++++--- test/functional/wallet_listdescriptors.py | 4 +++- test/functional/wallet_mnemonicbits.py | 7 ++++--- 7 files changed, 33 insertions(+), 12 deletions(-) diff --git a/src/wallet/rpc/backup.cpp b/src/wallet/rpc/backup.cpp index 260b872c4c61..7c817361d6f3 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. False if this descriptor is used to generate receiving addresses; defined only for active descriptors"}, {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,14 @@ RPCHelpMan listdescriptors() if (active && type != std::nullopt) { spk.pushKV("internal", wallet->GetScriptPubKeyMan(true) == desc_spk_man); } + if (type != std::nullopt) { + std::string match = strprintf("/9'/%s'/0'", Params().ExtCoinType()); + bool is_cj = descriptor.size() > 5 && descriptor.find(match) != std::string::npos; + if (is_cj) { + spk.pushKV("internal", false); + 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..a7adf5708375 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, InternalKey internal) { LOCK(cs_desc_man); assert(m_storage.IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS)); @@ -2099,10 +2099,10 @@ 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, internal == InternalKey::CoinJoin ? 9 : 44, Params().ExtCoinType()); std::string desc_suffix = "/*)"; - std::string internal_path = internal ? "/1" : "/0"; + std::string internal_path = (internal == InternalKey::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..273ddcf6fc8b 100644 --- a/src/wallet/scriptpubkeyman.h +++ b/src/wallet/scriptpubkeyman.h @@ -147,6 +147,13 @@ class CKeyPool } }; +enum class InternalKey +{ + External, + Internal, + 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, InternalKey internal); bool HavePrivateKeys() const override; diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index f5b38c98d579..469086122ada 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 internal : {InternalKey::External, InternalKey::Internal, InternalKey::CoinJoin}) { { // OUTPUT_TYPE is only one: LEGACY auto spk_manager = std::unique_ptr(new DescriptorScriptPubKeyMan(*this)); if (IsCrypted()) { @@ -3801,7 +3801,9 @@ void CWallet::SetupDescriptorScriptPubKeyMans(const SecureString& mnemonic_arg, spk_manager->SetupDescriptorGeneration(master_key, mnemonic, mnemonic_passphrase, internal); uint256 id = spk_manager->GetID(); m_spk_managers[id] = std::move(spk_manager); - AddActiveScriptPubKeyMan(id, internal); + if (internal != InternalKey::CoinJoin) { + AddActiveScriptPubKeyMan(id, internal == InternalKey::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..3c80745142ee 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']])) + self.log.info(f"result: {result['descriptors']}") assert_equal(1, len([d for d in result['descriptors'] if 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 From dd3a40c21da3cb48ac900bf1c9b806ca4631fe61 Mon Sep 17 00:00:00 2001 From: Konstantin Akimov Date: Fri, 5 Sep 2025 23:44:31 +0700 Subject: [PATCH 2/9] doc: adds release notes for CJ descriptor --- doc/release-notes-6835.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 doc/release-notes-6835.md 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) From fb47795e8568f40fe5ba19d4bf2af44f16b067e5 Mon Sep 17 00:00:00 2001 From: Konstantin Akimov Date: Mon, 22 Sep 2025 15:07:28 +0700 Subject: [PATCH 3/9] refactor: use named consts for derivation purpose 9 & 44 --- src/wallet/hdchain.cpp | 2 +- src/wallet/hdchain.h | 6 ++++++ src/wallet/scriptpubkeyman.cpp | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/wallet/hdchain.cpp b/src/wallet/hdchain.cpp index cc7fc0f76924..5e1098876cbb 100644 --- a/src/wallet/hdchain.cpp +++ b/src/wallet/hdchain.cpp @@ -203,6 +203,6 @@ 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/scriptpubkeyman.cpp b/src/wallet/scriptpubkeyman.cpp index a7adf5708375..28ecbbff3f68 100644 --- a/src/wallet/scriptpubkeyman.cpp +++ b/src/wallet/scriptpubkeyman.cpp @@ -2099,7 +2099,7 @@ bool DescriptorScriptPubKeyMan::SetupDescriptorGeneration(const CExtKey& master_ std::string xpub = EncodeExtPubKey(master_key.Neuter()); // Build descriptor string - std::string desc_prefix = strprintf("pkh(%s/%d'/%d'", xpub, internal == InternalKey::CoinJoin ? 9 : 44, Params().ExtCoinType()); + std::string desc_prefix = strprintf("pkh(%s/%d'/%d'", xpub, internal == InternalKey::CoinJoin ? BIP32_PURPOSE_FEATURE : BIP32_PURPOSE_STANDARD, Params().ExtCoinType()); std::string desc_suffix = "/*)"; std::string internal_path = (internal == InternalKey::Internal) ? "/1" : "/0"; From 286b0074066808bb3ba7223bba0f390abbcb05a2 Mon Sep 17 00:00:00 2001 From: Konstantin Akimov Date: Tue, 23 Sep 2025 21:31:52 +0700 Subject: [PATCH 4/9] refactor: drop useless check that string is longer than 5 chars It's already checked by find() --- src/wallet/rpc/backup.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wallet/rpc/backup.cpp b/src/wallet/rpc/backup.cpp index 7c817361d6f3..e21da3ca8422 100644 --- a/src/wallet/rpc/backup.cpp +++ b/src/wallet/rpc/backup.cpp @@ -2039,8 +2039,8 @@ RPCHelpMan listdescriptors() spk.pushKV("internal", wallet->GetScriptPubKeyMan(true) == desc_spk_man); } if (type != std::nullopt) { - std::string match = strprintf("/9'/%s'/0'", Params().ExtCoinType()); - bool is_cj = descriptor.size() > 5 && descriptor.find(match) != std::string::npos; + std::string match = strprintf("/%d'/%s'/0'", BIP32_PURPOSE_FEATURE, Params().ExtCoinType()); + bool is_cj = descriptor.find(match) != std::string::npos; if (is_cj) { spk.pushKV("internal", false); spk.pushKV("coinjoin", is_cj); From 859761cfcbb111de3091693d5cc48fface268ae8 Mon Sep 17 00:00:00 2001 From: Konstantin Akimov Date: Thu, 25 Sep 2025 14:09:28 +0700 Subject: [PATCH 5/9] refactor: rename InternalKey to PathDerivationType --- src/wallet/scriptpubkeyman.cpp | 6 +++--- src/wallet/scriptpubkeyman.h | 10 +++++----- src/wallet/wallet.cpp | 8 ++++---- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/wallet/scriptpubkeyman.cpp b/src/wallet/scriptpubkeyman.cpp index 28ecbbff3f68..8922ac00ae3b 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, InternalKey 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,10 @@ bool DescriptorScriptPubKeyMan::SetupDescriptorGeneration(const CExtKey& master_ std::string xpub = EncodeExtPubKey(master_key.Neuter()); // Build descriptor string - std::string desc_prefix = strprintf("pkh(%s/%d'/%d'", xpub, internal == InternalKey::CoinJoin ? BIP32_PURPOSE_FEATURE : BIP32_PURPOSE_STANDARD, Params().ExtCoinType()); + std::string desc_prefix = strprintf("pkh(%s/%d'/%d'", xpub, type == PathDerivationType::DIP0009_CoinJoin ? BIP32_PURPOSE_FEATURE : BIP32_PURPOSE_STANDARD, Params().ExtCoinType()); std::string desc_suffix = "/*)"; - std::string internal_path = (internal == InternalKey::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 273ddcf6fc8b..7f4ee75c413b 100644 --- a/src/wallet/scriptpubkeyman.h +++ b/src/wallet/scriptpubkeyman.h @@ -147,11 +147,11 @@ class CKeyPool } }; -enum class InternalKey +enum class PathDerivationType { - External, - Internal, - CoinJoin, + BIP44_External, + BIP44_Internal, + DIP0009_CoinJoin, }; /* @@ -582,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, InternalKey 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 469086122ada..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 (auto internal : {InternalKey::External, InternalKey::Internal, InternalKey::CoinJoin}) { + 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,11 +3798,11 @@ 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); - if (internal != InternalKey::CoinJoin) { - AddActiveScriptPubKeyMan(id, internal == InternalKey::Internal); + if (type != PathDerivationType::DIP0009_CoinJoin) { + AddActiveScriptPubKeyMan(id, type == PathDerivationType::BIP44_Internal); } } } From 32fd4611fe0a6ed16888c755834b2024ccd79474 Mon Sep 17 00:00:00 2001 From: Konstantin Akimov Date: Thu, 25 Sep 2025 14:13:19 +0700 Subject: [PATCH 6/9] fmt: clang format --- src/wallet/hdchain.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/wallet/hdchain.cpp b/src/wallet/hdchain.cpp index 5e1098876cbb..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/%d'/%d'/%d'/%d/%d", BIP32_PURPOSE_STANDARD, 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 From c821702423c4b1f7bac54f1fba7fee66236ef8f8 Mon Sep 17 00:00:00 2001 From: Konstantin Akimov Date: Tue, 30 Sep 2025 03:47:13 +0700 Subject: [PATCH 7/9] fix: derivation path for cj - add missing /4' (cj purpose) --- src/wallet/rpc/backup.cpp | 2 +- src/wallet/scriptpubkeyman.cpp | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/wallet/rpc/backup.cpp b/src/wallet/rpc/backup.cpp index e21da3ca8422..506053f0e57b 100644 --- a/src/wallet/rpc/backup.cpp +++ b/src/wallet/rpc/backup.cpp @@ -2039,7 +2039,7 @@ RPCHelpMan listdescriptors() spk.pushKV("internal", wallet->GetScriptPubKeyMan(true) == desc_spk_man); } if (type != std::nullopt) { - std::string match = strprintf("/%d'/%s'/0'", BIP32_PURPOSE_FEATURE, Params().ExtCoinType()); + 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("internal", false); diff --git a/src/wallet/scriptpubkeyman.cpp b/src/wallet/scriptpubkeyman.cpp index 8922ac00ae3b..28c354dd6997 100644 --- a/src/wallet/scriptpubkeyman.cpp +++ b/src/wallet/scriptpubkeyman.cpp @@ -2100,8 +2100,10 @@ bool DescriptorScriptPubKeyMan::SetupDescriptorGeneration(const CExtKey& master_ // Build descriptor string 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 = (type == PathDerivationType::BIP44_Internal) ? "/1" : "/0"; std::string desc_str = desc_prefix + "/0'" + internal_path + desc_suffix; From 8a49279541030356ebd7b1304977752b90b07eb2 Mon Sep 17 00:00:00 2001 From: Konstantin Akimov Date: Tue, 30 Sep 2025 03:51:37 +0700 Subject: [PATCH 8/9] fix: rpc improvements for CJ derivation path Co-authored-by: UdjinM6 --- src/wallet/rpc/backup.cpp | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/wallet/rpc/backup.cpp b/src/wallet/rpc/backup.cpp index 506053f0e57b..7c4d192bfa0d 100644 --- a/src/wallet/rpc/backup.cpp +++ b/src/wallet/rpc/backup.cpp @@ -1977,7 +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. 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. False if this descriptor is used to generate receiving addresses."}, {RPCResult::Type::ARR_FIXED, "range", /*optional=*/true, "Defined only for ranged descriptors", { {RPCResult::Type::NUM, "", "Range start inclusive"}, {RPCResult::Type::NUM, "", "Range end inclusive"}, @@ -2041,10 +2041,7 @@ RPCHelpMan listdescriptors() 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("internal", false); - spk.pushKV("coinjoin", is_cj); - } + spk.pushKV("coinjoin", is_cj); } if (wallet_descriptor.descriptor->IsRange()) { UniValue range(UniValue::VARR); From b957689d6edca231bdf398e4bb124687814da3d4 Mon Sep 17 00:00:00 2001 From: Konstantin Akimov Date: Tue, 30 Sep 2025 13:47:00 +0700 Subject: [PATCH 9/9] fix: functional tests after RPC changes --- src/wallet/rpc/backup.cpp | 6 ++++-- test/functional/wallet_listdescriptors.py | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/wallet/rpc/backup.cpp b/src/wallet/rpc/backup.cpp index 7c4d192bfa0d..1e3cf53c5293 100644 --- a/src/wallet/rpc/backup.cpp +++ b/src/wallet/rpc/backup.cpp @@ -1977,7 +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. False if this descriptor is used to generate receiving addresses."}, + {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"}, @@ -2041,7 +2041,9 @@ RPCHelpMan listdescriptors() 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; - spk.pushKV("coinjoin", is_cj); + if (is_cj) { + spk.pushKV("coinjoin", is_cj); + } } if (wallet_descriptor.descriptor->IsRange()) { UniValue range(UniValue::VARR); diff --git a/test/functional/wallet_listdescriptors.py b/test/functional/wallet_listdescriptors.py index 3c80745142ee..bd0c18d18e64 100755 --- a/test/functional/wallet_listdescriptors.py +++ b/test/functional/wallet_listdescriptors.py @@ -49,7 +49,7 @@ def run_test(self): assert_equal(3, len(result['descriptors'])) assert_equal(2, len([d for d in result['descriptors'] if d['active']])) self.log.info(f"result: {result['descriptors']}") - assert_equal(1, len([d for d in result['descriptors'] if d['internal']])) + 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'] != ''