Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
9 changes: 9 additions & 0 deletions doc/release-notes-6870.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Wallet
------

- Add `-coinjoinfreshchange` option to control change destination behavior
during CoinJoin denomination creation. By default (flag unset), change is
sent back to the source address (legacy behavior). When enabled, change is
sent to a fresh change address to avoid address/public key reuse. (#6870)


8 changes: 8 additions & 0 deletions src/coinjoin/options.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,18 @@ void CCoinJoinClientOptions::SetDenomsHardCap(int denoms_hardcap)
options.nCoinJoinDenomsHardCap = denoms_hardcap;
}

void CCoinJoinClientOptions::SetFreshChange(bool fresh_change)
{
CCoinJoinClientOptions& options = CCoinJoinClientOptions::Get();
options.fCoinJoinFreshChange = fresh_change;
}

void CCoinJoinClientOptions::Init()
{
assert(!CCoinJoinClientOptions::_instance);
static CCoinJoinClientOptions instance;
instance.fCoinJoinMultiSession = gArgs.GetBoolArg("-coinjoinmultisession", DEFAULT_COINJOIN_MULTISESSION);
instance.fCoinJoinFreshChange = gArgs.GetBoolArg("-coinjoinfreshchange", DEFAULT_COINJOIN_FRESHCHANGE);
instance.nCoinJoinSessions = std::min(std::max((int)gArgs.GetIntArg("-coinjoinsessions", DEFAULT_COINJOIN_SESSIONS), MIN_COINJOIN_SESSIONS), MAX_COINJOIN_SESSIONS);
instance.nCoinJoinRounds = std::min(std::max((int)gArgs.GetIntArg("-coinjoinrounds", DEFAULT_COINJOIN_ROUNDS), MIN_COINJOIN_ROUNDS), MAX_COINJOIN_ROUNDS);
instance.nCoinJoinAmount = std::min(std::max((int)gArgs.GetIntArg("-coinjoinamount", DEFAULT_COINJOIN_AMOUNT), MIN_COINJOIN_AMOUNT), MAX_COINJOIN_AMOUNT);
Expand All @@ -85,4 +92,5 @@ void CCoinJoinClientOptions::GetJsonInfo(UniValue& obj)
obj.pushKV("max_amount", options.nCoinJoinAmount.load());
obj.pushKV("denoms_goal", options.nCoinJoinDenomsGoal.load());
obj.pushKV("denoms_hardcap", options.nCoinJoinDenomsHardCap.load());
obj.pushKV("fresh_change", options.fCoinJoinFreshChange.load());
}
4 changes: 4 additions & 0 deletions src/coinjoin/options.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ static constexpr int DEFAULT_COINJOIN_DENOMS_HARDCAP = 300;

static constexpr bool DEFAULT_COINJOIN_AUTOSTART = false;
static constexpr bool DEFAULT_COINJOIN_MULTISESSION = false;
static constexpr bool DEFAULT_COINJOIN_FRESHCHANGE = false;

// How many new denom outputs to create before we consider the "goal" loop in CreateDenominated
// a final one and start creating an actual tx. Same limit applies for the "hard cap" part of the algo.
Expand Down Expand Up @@ -60,6 +61,7 @@ class CCoinJoinClientOptions
static int GetAmount() { return CCoinJoinClientOptions::Get().nCoinJoinAmount; }
static int GetDenomsGoal() { return CCoinJoinClientOptions::Get().nCoinJoinDenomsGoal; }
static int GetDenomsHardCap() { return CCoinJoinClientOptions::Get().nCoinJoinDenomsHardCap; }
static bool GetFreshChange() { return CCoinJoinClientOptions::Get().fCoinJoinFreshChange; }

static void SetEnabled(bool fEnabled);
static void SetMultiSessionEnabled(bool fEnabled);
Expand All @@ -68,6 +70,7 @@ class CCoinJoinClientOptions
static void SetAmount(CAmount amount);
static void SetDenomsGoal(int denoms_goal);
static void SetDenomsHardCap(int denoms_hardcap);
static void SetFreshChange(bool fresh_change);

static bool IsEnabled() { return CCoinJoinClientOptions::Get().fEnableCoinJoin; }
static bool IsMultiSessionEnabled() { return CCoinJoinClientOptions::Get().fCoinJoinMultiSession; }
Expand All @@ -86,6 +89,7 @@ class CCoinJoinClientOptions
std::atomic<int> nCoinJoinDenomsHardCap{DEFAULT_COINJOIN_DENOMS_HARDCAP};
std::atomic<bool> fEnableCoinJoin{false};
std::atomic<bool> fCoinJoinMultiSession{DEFAULT_COINJOIN_MULTISESSION};
std::atomic<bool> fCoinJoinFreshChange{DEFAULT_COINJOIN_FRESHCHANGE};

CCoinJoinClientOptions() = default;

Expand Down
9 changes: 7 additions & 2 deletions src/coinjoin/util.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

#include <coinjoin/util.h>
#include <policy/fees.h>
#include <coinjoin/options.h>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

❓ Verification inconclusive

Functional change LGTM; fix formatting to unblock CI

  • Behavior toggle is correct: only set destChange when fresh-change is disabled; otherwise let wallet choose a fresh change address.
  • CI is failing on clang-format for this file. Please run the project’s formatter to resolve.

To fix:

  • Run contrib/devtools/clang-format-diff.py as in the CI message, or clang-format the file to project style.

Also, please sanity-check keypool usage with fresh-change enabled (one extra key per tx) to ensure no unexpected keypool depletion during CreateDenoms.

Also applies to: 129-134


Fix formatting in src/coinjoin/util.cpp to unblock CI

  • Apply contrib/devtools/clang-format-diff.py (or run clang-format) on this file.
  • Sanity-check keypool consumption with fresh-change enabled (one extra key per tx) to avoid depletion during CreateDenoms.
🤖 Prompt for AI Agents
In src/coinjoin/util.cpp around line 7, the file needs clang-format applied and
the keypool consumption logic must account for fresh-change using one extra key
per transaction; run contrib/devtools/clang-format-diff.py or clang-format on
this file to fix formatting, then update the sanity-check that verifies keypool
availability during CreateDenoms to reserve one additional key per tx when
fresh-change is enabled (or decrement the available key count by one per tx) so
the check reflects the true consumption and prevents depletion.

#include <policy/policy.h>
#include <util/translation.h>
#include <wallet/fees.h>
Expand Down Expand Up @@ -125,8 +126,12 @@ CTransactionBuilder::CTransactionBuilder(CWallet& wallet, const CompactTallyItem
coinControl.m_discard_feerate = ::GetDiscardRate(m_wallet);
// Generate a feerate which will be used by calculations of this class and also by CWallet::CreateTransaction
coinControl.m_feerate = std::max(GetRequiredFeeRate(m_wallet), m_wallet.m_pay_tx_fee);
// Change always goes back to origin
coinControl.destChange = tallyItemIn.txdest;
// By default, keep legacy behavior: change goes back to the origin address.
// When -coinjoinfreshchange is enabled, let the wallet select a fresh
// change destination to avoid address reuse.
if (!CCoinJoinClientOptions::GetFreshChange()) {
coinControl.destChange = tallyItemIn.txdest;
}
// Only allow tallyItems inputs for tx creation
coinControl.m_allow_other_inputs = false;
// Create dummy tx to calculate the exact required fees upfront for accurate amount and fee calculations
Expand Down
1 change: 1 addition & 0 deletions src/wallet/init.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ void WalletInit::AddWalletOptions(ArgsManager& argsman) const
argsman.AddArg("-coinjoinmultisession", strprintf("Enable multiple CoinJoin mixing sessions per block, experimental (0-1, default: %u)", DEFAULT_COINJOIN_MULTISESSION), ArgsManager::ALLOW_ANY, OptionsCategory::WALLET_COINJOIN);
argsman.AddArg("-coinjoinrounds=<n>", strprintf("Use N separate masternodes for each denominated input to mix funds (%u-%u, default: %u)", MIN_COINJOIN_ROUNDS, MAX_COINJOIN_ROUNDS, DEFAULT_COINJOIN_ROUNDS), ArgsManager::ALLOW_ANY, OptionsCategory::WALLET_COINJOIN);
argsman.AddArg("-coinjoinsessions=<n>", strprintf("Use N separate masternodes in parallel to mix funds (%u-%u, default: %u)", MIN_COINJOIN_SESSIONS, MAX_COINJOIN_SESSIONS, DEFAULT_COINJOIN_SESSIONS), ArgsManager::ALLOW_ANY, OptionsCategory::WALLET_COINJOIN);
argsman.AddArg("-coinjoinfreshchange", strprintf("Send change from denomination creation to a fresh change address instead of the source address (0-1, default: %u)", DEFAULT_COINJOIN_FRESHCHANGE), ArgsManager::ALLOW_ANY, OptionsCategory::WALLET_COINJOIN);

#ifdef USE_BDB
argsman.AddArg("-dblogsize=<n>", strprintf("Flush wallet database activity from memory to disk log every <n> megabytes (default: %u)", DatabaseOptions().max_log_mb), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::WALLET_DEBUG_TEST);
Expand Down
Loading