Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions doc/release-notes-6835.md
Original file line number Diff line number Diff line change
@@ -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)
3 changes: 2 additions & 1 deletion src/wallet/hdchain.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
6 changes: 6 additions & 0 deletions src/wallet/hdchain.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
9 changes: 9 additions & 0 deletions src/wallet/rpc/backup.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
// file COPYING or http://www.opensource.org/licenses/mit-license.php.

#include <chain.h>
#include <chainparams.h>
#include <clientversion.h>
#include <core_io.h>
#include <fs.h>
Expand Down Expand Up @@ -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"},
Expand Down Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion src/wallet/rpc/wallet.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
Expand Down
10 changes: 6 additions & 4 deletions src/wallet/scriptpubkeyman.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand All @@ -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
Expand Down
9 changes: 8 additions & 1 deletion src/wallet/scriptpubkeyman.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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;

Expand Down
9 changes: 5 additions & 4 deletions src/wallet/wallet.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<DescriptorScriptPubKeyMan>(new DescriptorScriptPubKeyMan(*this));
if (IsCrypted()) {
Expand All @@ -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);
}
}
}
}
Expand All @@ -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;
}
Expand Down
6 changes: 4 additions & 2 deletions test/functional/wallet_listdescriptors.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 4 additions & 3 deletions test/functional/wallet_mnemonicbits.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Comment on lines 51 to +57
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Update comment to reflect three mnemonic descriptors.

The comment states "Two other should be active and have mnemonic" but the code now expects 3 mnemonic descriptors (line 55). The comment should be updated to match the new behavior.

Apply this diff to fix the comment:

-            # 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
+            # There should be 4 descriptors in total
+            # One is an inactive imported private key for coinbase (no mnemonic)
+            # Three others have mnemonics: two active (internal, external) and one inactive (coinjoin)
             assert_equal(mnemonic_count, 3)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# 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)
# There should be 4 descriptors in total
# One is an inactive imported private key for coinbase (no mnemonic)
# Three others have mnemonics: two active (internal, external) and one inactive (coinjoin)
assert_equal(mnemonic_count, 3)
assert_equal(cb_count, 1)
assert_equal(len(descriptors), 4)
🤖 Prompt for AI Agents
In test/functional/wallet_mnemonicbits.py around lines 52 to 57, the inline
comment incorrectly states "Two other should be active and have mnemonic" while
the assertion expects 3 mnemonic descriptors; update the comment to reflect that
there are three mnemonic descriptors (e.g., "Three other should be active and
have mnemonic") so the comment matches the assertion expectations.

else:
assert_equal(len(self.nodes[0].dumphdinfo()["mnemonic"].split()), 12) # 12 words by default
# legacy HD wallets could have only one chain
Expand Down
Loading