Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
49d7d5f
feat: coinjoin promotion / demotion
PastaPastaPasta Jul 10, 2026
3d6d49a
test: cover CoinJoin promotion/demotion validation and decision logic
PastaPastaPasta Jul 10, 2026
33702b4
docs: add release notes for pr 7052
PastaPastaPasta Jul 10, 2026
9c29052
feat(coinjoin): gate rebalance sessions and require session-denom cover
PastaPastaPasta Aug 1, 2026
363d305
fix(coinjoin): tolerate one-block tip skew only at the V24 boundary
PastaPastaPasta Aug 1, 2026
efff25c
fix(coinjoin): only announce unbalanced DSTXes to peers that support …
PastaPastaPasta Aug 1, 2026
9cae6f4
feat(coinjoin): reset mixing rounds on promotion/demotion outputs
PastaPastaPasta Aug 1, 2026
9e83d6a
fix(coinjoin): don't announce oversized balanced DSTXes to legacy peers
PastaPastaPasta Aug 17, 2026
88d4e6b
fix(coinjoin): guard against null prevtx in GetRealOutpointCoinJoinRo…
PastaPastaPasta Aug 17, 2026
9727337
fix(coinjoin): keep relaying islocks when withholding a DSTX from leg…
PastaPastaPasta Aug 17, 2026
50c62c9
fix(coinjoin): annotate m_fRebalanceSession as GUARDED_BY(cs_coinjoin)
PastaPastaPasta Aug 17, 2026
f7d2bbb
fix(coinjoin): bind entry admission and charging to the validated ses…
PastaPastaPasta Aug 18, 2026
1ff74e9
fix(coinjoin): latch rebalance capability on admission, not on creato…
PastaPastaPasta Aug 18, 2026
032bc99
fix(coinjoin): widen the V24 DSTX skew tolerance and downgrade withhe…
PastaPastaPasta Aug 18, 2026
428eb97
fix(coinjoin): recognize unbalanced mixing transactions as denominated
PastaPastaPasta Aug 18, 2026
6af461c
fix(coinjoin): scale the rebalance gap threshold with the denoms goal
PastaPastaPasta Aug 18, 2026
e78b965
test(coinjoin): cover session finalization race and gap-threshold bou…
PastaPastaPasta Aug 18, 2026
5748316
refactor(coinjoin): use static_cast in new promotion/demotion code
PastaPastaPasta Aug 18, 2026
0dc132f
docs: describe rebalance session gating and DSTX downgrade accurately
PastaPastaPasta Aug 18, 2026
3646c95
fix(coinjoin): tolerate DSTX tip skew from V24 lock-in, and address r…
PastaPastaPasta Aug 18, 2026
e6687d9
fix(coinjoin): tolerate arbitrary tip skew when validating a final tr…
PastaPastaPasta Aug 18, 2026
f232f02
fix(coinjoin): keep the V24 tip-skew tolerance at one block
PastaPastaPasta Aug 18, 2026
123858d
fix(coinjoin): don't burn queue announcements a rebalance pass skips
UdjinM6 Aug 17, 2026
26d0a98
fix(coinjoin): tell participants when an uncovered session is reset
UdjinM6 Aug 17, 2026
adb2765
fix(coinjoin): bound the sides of a post-V24 DSTX against each other
UdjinM6 Aug 18, 2026
d1fca89
fix(coinjoin): decide the pool state from one consistent snapshot
PastaPastaPasta Aug 19, 2026
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
45 changes: 45 additions & 0 deletions doc/release-notes-7052.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
P2P and network changes
-----------------------

- The protocol version was bumped to 70241. The `dsa` message gained a
version-gated flags field declaring which mixing direction a
participant intends. A session commits to carrying promotion/demotion
entries only once a participant is admitted that actually declared one,
and it becomes closed to pre-70241 clients only from that point on;
conversely, a session that has already admitted a pre-70241 client
refuses later promotion/demotion participants. Either way the refusal
happens at acceptance time, before any collateral is committed, so a
client doing ordinary 1:1 mixing is never turned away from a session
simply because of who opened it. Unbalanced (promotion/demotion) DSTXes
are only announced as `dstx` to peers at protocol 70241 or newer, since
older peers would reject them as structurally invalid and penalize the
relayer; those peers are sent a plain `tx` announcement instead, so
they still receive the transaction without the mixing metadata they
cannot parse. (#7052)

- A mixing session only completes once each side of its denomination is
occupied by nobody or by at least two participants, since coins are
only concealed by other coins of the same size on the same side. A
session that attracts a lone promotion or demotion participant and no
counterpart therefore waits, and expires in the queue stage without
charging anyone's collateral, rather than publishing a transaction that
would identify that participant's coins. Because admission relies on
the declared directions, a participant whose entry deviates from what
it declared has its collateral consumed. (#7052)

Wallet
------

- CoinJoin can now promote and demote between adjacent standard
denominations within a mixing session after V24 activation.
Promotion combines 10 inputs of one denomination into 1 output of the
next larger denomination, while demotion splits 1 input into 10
outputs of the next smaller denomination. Pre-V24 behavior remains
unchanged. (#7052)

- Conversions only spend fully-mixed coins, and their outputs start
mixing over from zero rounds. The 10:1 shape of a conversion publicly
clusters one participant's coins even inside a mixing transaction, so
a converted coin is not treated as mixed: it re-enters mixing at its
new denomination and disperses normally, while the histories of the
fully-mixed coins that fed the conversion remain protected. (#7052)
573 changes: 544 additions & 29 deletions src/coinjoin/client.cpp

Large diffs are not rendered by default.

46 changes: 44 additions & 2 deletions src/coinjoin/client.h
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,11 @@ class CCoinJoinClientSession : public CCoinJoinBaseSession

CKeyHolderStorage keyHolderStorage; // storage for keys used in PrepareDenominate

// Post-V24: Promotion/demotion session state
bool m_fPromotion{false}; // True if this session is promoting smaller -> larger denom
bool m_fDemotion{false}; // True if this session is demoting larger -> smaller denom
std::vector<COutPoint> m_vecRebalanceInputs; // Selected inputs for promotion/demotion rebalancing

/// Create denominations
bool CreateDenominated(CAmount nBalanceToDenominate);
bool CreateDenominated(CAmount nBalanceToDenominate, const wallet::CompactTallyItem& tallyItem, bool fCreateMixingCollaterals)
Expand All @@ -102,16 +107,35 @@ class CCoinJoinClientSession : public CCoinJoinBaseSession
bool CreateCollateralTransaction(CMutableTransaction& txCollateral, std::string& strReason)
EXCLUSIVE_LOCKS_REQUIRED(m_wallet->cs_wallet);

bool JoinExistingQueue(CAmount nBalanceNeedsAnonymized, CConnman& connman);
bool JoinExistingQueue(CAmount nBalanceNeedsAnonymized, CConnman& connman,
int nTargetDenom = 0, bool fPromotion = false, bool fDemotion = false);
bool StartNewQueue(CAmount nBalanceNeedsAnonymized, CConnman& connman);
bool StartNewQueue(CAmount nBalanceNeedsAnonymized, CConnman& connman,
int nTargetDenom, bool fPromotion, bool fDemotion);
CDeterministicMNCPtr GetRandomNotUsedMasternode();

/// Post-V24: select and lock inputs for a promotion/demotion session. Locked outpoints are
/// recorded in m_vecRebalanceInputs and vecOutPointLocked so UnlockCoins() releases them on
/// any failure path.
bool SelectRebalanceInputs(int nTargetDenom, bool fPromotion, std::vector<CTxDSIn>& vecTxDSInRet);
/// Post-V24: unlock and forget the inputs selected by SelectRebalanceInputs (session setup failed)
void UnlockRebalanceInputs();

/// step 0: select denominated inputs and txouts
bool SelectDenominate(std::string& strErrorRet, std::vector<CTxDSIn>& vecTxDSInRet);
/// step 1: prepare denominated inputs and outputs
bool PrepareDenominate(int nMinRounds, int nMaxRounds, std::string& strErrorRet, const std::vector<CTxDSIn>& vecTxDSIn,
std::vector<std::pair<CTxDSIn, CTxOut>>& vecPSInOutPairsRet, bool fDryRun = false)
EXCLUSIVE_LOCKS_REQUIRED(m_wallet->cs_wallet);

/// Post-V24: prepare promotion entry (10 inputs of smaller denom -> 1 output of larger denom)
bool PreparePromotionEntry(std::string& strErrorRet, std::vector<std::pair<CTxDSIn, CTxOut>>& vecPSInOutPairsRet)
EXCLUSIVE_LOCKS_REQUIRED(m_wallet->cs_wallet);

/// Post-V24: prepare demotion entry (1 input of larger denom -> 10 outputs of smaller denom)
bool PrepareDemotionEntry(std::string& strErrorRet, std::vector<std::pair<CTxDSIn, CTxOut>>& vecPSInOutPairsRet)
EXCLUSIVE_LOCKS_REQUIRED(m_wallet->cs_wallet);

/// step 2: send denominated inputs and outputs prepared in step 1
bool SendDenominate(const std::vector<std::pair<CTxDSIn, CTxOut> >& vecPSInOutPairsIn, CConnman& connman) EXCLUSIVE_LOCKS_REQUIRED(!cs_coinjoin);

Expand Down Expand Up @@ -226,7 +250,7 @@ class CCoinJoinClientManager : public interfaces::CoinJoin::Client

bool TrySubmitDenominate(const uint256& proTxHash, CConnman& connman) EXCLUSIVE_LOCKS_REQUIRED(!cs_deqsessions);
bool MarkAlreadyJoinedQueueAsTried(CCoinJoinQueue& dsq) const EXCLUSIVE_LOCKS_REQUIRED(!cs_deqsessions);
bool GetQueueItemAndTry(CCoinJoinQueue& dsq) const;
bool GetQueueItemAndTry(CCoinJoinQueue& dsq, int nDenomFilter = 0) const;

void CheckTimeout() EXCLUSIVE_LOCKS_REQUIRED(!cs_deqsessions);

Expand Down Expand Up @@ -256,6 +280,24 @@ class CCoinJoinClientManager : public interfaces::CoinJoin::Client
bool isMixing() const override;
bool startMixing() override;
void stopMixing() override;

/**
* Post-V24: Check if we should promote smaller denominations into larger ones
* @param nSmallerDenom The smaller denomination to promote from
* @param nLargerDenom The larger denomination to promote into
* @param counts Wallet denomination counts, e.g. from CWallet::GetDenominationCounts()
* @return true if promotion is recommended
*/
static bool ShouldPromote(int nSmallerDenom, int nLargerDenom, const wallet::CoinJoinDenomCounts& counts);

/**
* Post-V24: Check if we should demote larger denominations into smaller ones
* @param nLargerDenom The larger denomination to demote from
* @param nSmallerDenom The smaller denomination to demote into
* @param counts Wallet denomination counts, e.g. from CWallet::GetDenominationCounts()
* @return true if demotion is recommended
*/
static bool ShouldDemote(int nLargerDenom, int nSmallerDenom, const wallet::CoinJoinDenomCounts& counts);
};

#endif // BITCOIN_COINJOIN_CLIENT_H
Loading
Loading