Skip to content
Open
Show file tree
Hide file tree
Changes from 7 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-7052.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
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)
557 changes: 543 additions & 14 deletions src/coinjoin/client.cpp

Large diffs are not rendered by default.

35 changes: 34 additions & 1 deletion src/coinjoin/client.h
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,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_vecPromotionInputs; // Selected inputs for promotion (10 coins)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💬 Nitpick: m_vecPromotionInputs is reused for demotion inputs, naming is misleading

m_vecPromotionInputs stores inputs for both promotion (10 coins) and demotion (1 coin), as seen at client.cpp:1290-1292 and 1825-1839. The name and comment ("Selected inputs for promotion (10 coins)") imply promotion-only usage. Consider renaming to m_vecMixingInputs or m_vecSessionInputs.

source: ['claude']


/// Create denominations
bool CreateDenominated(CAmount nBalanceToDenominate);
bool CreateDenominated(CAmount nBalanceToDenominate, const wallet::CompactTallyItem& tallyItem, bool fCreateMixingCollaterals)
Expand All @@ -156,15 +161,27 @@ 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);

/// 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 @@ -315,6 +332,22 @@ class CCoinJoinClientManager
EXCLUSIVE_LOCKS_REQUIRED(!cs_deqsessions);

void GetJsonInfo(UniValue& obj) const EXCLUSIVE_LOCKS_REQUIRED(!cs_deqsessions);

/**
* 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
* @return true if promotion is recommended
*/
bool ShouldPromote(int nSmallerDenom, int nLargerDenom) const;

/**
* 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
* @return true if demotion is recommended
*/
bool ShouldDemote(int nLargerDenom, int nSmallerDenom) const;
};

#endif // BITCOIN_COINJOIN_CLIENT_H
223 changes: 207 additions & 16 deletions src/coinjoin/coinjoin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

#include <chain.h>
#include <chainparams.h>
#include <deploymentstatus.h>
#include <txmempool.h>
#include <util/moneystr.h>
#include <util/system.h>
Expand Down Expand Up @@ -82,24 +83,54 @@ bool CCoinJoinBroadcastTx::CheckSignature(const CBLSPublicKey& blsPubKey) const
return true;
}

bool CCoinJoinBroadcastTx::IsValidStructure() const
bool CCoinJoinBroadcastTx::IsExpired(const CBlockIndex* pindex, const llmq::CChainLocksHandler& clhandler) const
{
// expire confirmed DSTXes after ~1h since confirmation or chainlocked confirmation
if (!nConfirmedHeight.has_value() || pindex->nHeight < *nConfirmedHeight) return false; // not mined yet
if (pindex->nHeight - *nConfirmedHeight > 24) return true; // mined more than an hour ago
return clhandler.HasChainLock(pindex->nHeight, *pindex->phashBlock);
}

bool CCoinJoinBroadcastTx::IsValidStructure(const CBlockIndex* pindex) const
{
// some trivial checks only
if (masternodeOutpoint.IsNull() && m_protxHash.IsNull()) {
return false;
}
if (tx->vin.size() != tx->vout.size()) {

const bool fV24Active = pindex && DeploymentActiveAt(*pindex, Params().GetConsensus(), Consensus::DEPLOYMENT_V24);

// Pre-V24: require balanced input/output counts (1:1 mixing only)
// Post-V24: allow unbalanced counts (promotion/demotion)
if (!fV24Active && tx->vin.size() != tx->vout.size()) {
return false;
}

if (tx->vin.size() < size_t(CoinJoin::GetMinPoolParticipants())) {
return false;
}
if (tx->vin.size() > CoinJoin::GetMaxPoolParticipants() * COINJOIN_ENTRY_MAX_SIZE) {

// Post-V24: allow up to 200 inputs (20 participants * 10 inputs for promotions)
// Pre-V24: max 180 inputs (20 participants * 9 entries)
const size_t nMaxInputs = fV24Active
? CoinJoin::GetMaxPoolParticipants() * CoinJoin::PROMOTION_RATIO
: CoinJoin::GetMaxPoolParticipants() * COINJOIN_ENTRY_MAX_SIZE;

if (tx->vin.size() > nMaxInputs) {
return false;
}
return std::ranges::all_of(tx->vout, [](const auto& txOut) {

if (!std::ranges::all_of(tx->vout, [](const auto& txOut) {
return CoinJoin::IsDenominatedAmount(txOut.nValue) && txOut.scriptPubKey.IsPayToPublicKeyHash();
});
})) {
return false;
}
Comment on lines +103 to +127
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Restore an explicit post-V24 vout cap here.

Pre-V24 the vin.size() == vout.size() rule bounded outputs implicitly. After this change, any number of denominated P2PKH outputs passes IsValidStructure() as long as the input count is within nMaxInputs, even though valid post-V24 sessions still top out at GetMaxPoolParticipants() * CoinJoin::PROMOTION_RATIO outputs.

🧱 Restore the structural cap
-    if (tx->vin.size() > nMaxInputs) {
+    if (tx->vin.size() > nMaxInputs || tx->vout.size() > nMaxInputs) {
         return false;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/coinjoin/coinjoin.cpp` around lines 103 - 127, Add an explicit post‑V24
cap on outputs: after computing nMaxInputs (which uses fV24Active,
CoinJoin::GetMaxPoolParticipants() and CoinJoin::PROMOTION_RATIO), add a check
that tx->vout.size() <= nMaxInputs and return false if it exceeds it; keep the
existing denominated P2PKH validation unchanged so IsValidStructure()/the
surrounding logic enforces both input and output caps.


// Note: For post-V24 unbalanced transactions (promotion/demotion),
// value sum validation (inputs == outputs) requires UTXO access and
// is performed in IsValidInOuts() when the transaction is processed.

return true;
}

void CCoinJoinBaseSession::SetNull()
Expand Down Expand Up @@ -185,17 +216,80 @@ bool CCoinJoinBaseSession::IsValidInOuts(CChainState& active_chainstate, const l
nMessageIDRet = MSG_NOERR;
if (fConsumeCollateralRet) *fConsumeCollateralRet = false;

if (vin.size() != vout.size()) {
// Check if V24 is active for promotion/demotion support
bool fV24Active{false};
{
LOCK(::cs_main);
const CBlockIndex* pindex = active_chainstate.m_chain.Tip();
fV24Active = pindex && DeploymentActiveAt(*pindex, Params().GetConsensus(), Consensus::DEPLOYMENT_V24);
}

// Determine entry type based on input/output counts
// Standard: N inputs, N outputs (same denom)
// Promotion: PROMOTION_RATIO inputs of session denom, 1 output of larger adjacent denom
// Demotion: 1 input of larger adjacent denom, PROMOTION_RATIO outputs of session denom
enum class EntryType { STANDARD, PROMOTION, DEMOTION, INVALID };
EntryType entryType = EntryType::STANDARD;

if (vin.size() == vout.size()) {
entryType = EntryType::STANDARD;
} else if (fV24Active) {
if (vin.size() == static_cast<size_t>(CoinJoin::PROMOTION_RATIO) && vout.size() == 1) {
entryType = EntryType::PROMOTION;
} else if (vin.size() == 1 && vout.size() == static_cast<size_t>(CoinJoin::PROMOTION_RATIO)) {
entryType = EntryType::DEMOTION;
} else {
entryType = EntryType::INVALID;
}
} else {
// Pre-V24: only standard entries allowed
LogPrint(BCLog::COINJOIN, "CCoinJoinBaseSession::%s -- ERROR: inputs vs outputs size mismatch! %d vs %d\n", __func__, vin.size(), vout.size());
nMessageIDRet = ERR_SIZE_MISMATCH;
if (fConsumeCollateralRet) *fConsumeCollateralRet = true;
return false;
}

auto checkTxOut = [&](const CTxOut& txout) {
if (int nDenom = CoinJoin::AmountToDenomination(txout.nValue); nDenom != nSessionDenom) {
LogPrint(BCLog::COINJOIN, "CCoinJoinBaseSession::IsValidInOuts -- ERROR: incompatible denom %d (%s) != nSessionDenom %d (%s)\n",
nDenom, CoinJoin::DenominationToString(nDenom), nSessionDenom, CoinJoin::DenominationToString(nSessionDenom));
if (entryType == EntryType::INVALID) {
LogPrint(BCLog::COINJOIN, "CCoinJoinBaseSession::%s -- ERROR: invalid entry structure! %d inputs, %d outputs\n", __func__, vin.size(), vout.size());
nMessageIDRet = ERR_SIZE_MISMATCH;
if (fConsumeCollateralRet) *fConsumeCollateralRet = true;
return false;
}

const int nLargerAdjacentDenom = CoinJoin::GetLargerAdjacentDenom(nSessionDenom);

// Determine expected denominations based on entry type
int nExpectedInputDenom = nSessionDenom;
int nExpectedOutputDenom = nSessionDenom;

if (entryType == EntryType::PROMOTION) {
// Promotion: inputs = session denom (smaller), output = larger adjacent
nExpectedInputDenom = nSessionDenom;
nExpectedOutputDenom = nLargerAdjacentDenom;
if (nLargerAdjacentDenom == 0) {
LogPrint(BCLog::COINJOIN, "CCoinJoinBaseSession::%s -- ERROR: no larger adjacent denom for promotion\n", __func__);
nMessageIDRet = ERR_DENOM;
if (fConsumeCollateralRet) *fConsumeCollateralRet = true;
return false;
}
} else if (entryType == EntryType::DEMOTION) {
// Demotion: input = larger adjacent, outputs = session denom (smaller)
nExpectedInputDenom = nLargerAdjacentDenom;
nExpectedOutputDenom = nSessionDenom;
if (nLargerAdjacentDenom == 0) {
LogPrint(BCLog::COINJOIN, "CCoinJoinBaseSession::%s -- ERROR: no larger adjacent denom for demotion\n", __func__);
nMessageIDRet = ERR_DENOM;
if (fConsumeCollateralRet) *fConsumeCollateralRet = true;
return false;
}
}
Comment on lines +227 to +276
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

IsValidInOuts() now rejects valid mixed-session final transactions.

The new EntryType switch and single expected-denomination pair assume the whole vin/vout set is either STANDARD, 10→1, or 1→10. That matches single-entry validation, but src/coinjoin/client.cpp, Lines 616-617 call this on the full final transaction, and src/coinjoin/server.cpp, Lines 300-346 can now aggregate standard plus rebalance entries into one tx; a valid 3-standard + 1-promotion session becomes 13 inputs / 4 outputs and hits EntryType::INVALID, so clients refuse to sign. This needs a separate final-transaction validator instead of reusing the per-entry classifier.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/coinjoin/coinjoin.cpp` around lines 227 - 276, IsValidInOuts() now treats
the whole vin/vout as one EntryType (EntryType enum) and rejects mixed-session
final transactions (e.g. multiple STANDARD entries plus a PROMOTION), causing
clients/servers (client.cpp and server.cpp callers) to fail signing; fix by
restoring per-entry validation or adding a dedicated final-transaction
validator: either (A) change the callers in client.cpp/server.cpp to call the
existing per-entry validators (CoinJoin::ValidatePromotionEntry,
CoinJoin::ValidateDemotionEntry, or the STANDARD path) for each logical entry
instead of passing the whole tx into IsValidInOuts(), or (B) implement a new
function (e.g., CoinJoin::ValidateFinalTransaction) that parses the combined
vin/vout into constituent entries, validates each entry with the existing
ValidatePromotionEntry/ValidateDemotionEntry logic, and determines expected
denominations using CoinJoin::GetLargerAdjacentDenom per entry; update
IsValidInOuts() to only validate single entries or delegate to the new
ValidateFinalTransaction when given a full-final-tx context so mixed entries are
accepted.


auto checkTxOut = [&](const CTxOut& txout, int nExpectedDenom) {
const int nDenom = CoinJoin::AmountToDenomination(txout.nValue);

if (nDenom != nExpectedDenom) {
LogPrint(BCLog::COINJOIN, "CCoinJoinBaseSession::IsValidInOuts -- ERROR: incompatible denom %d (%s) != expected %d (%s)\n",
nDenom, CoinJoin::DenominationToString(nDenom), nExpectedDenom, CoinJoin::DenominationToString(nExpectedDenom));
nMessageIDRet = ERR_DENOM;
if (fConsumeCollateralRet) *fConsumeCollateralRet = true;
return false;
Expand All @@ -206,21 +300,20 @@ bool CCoinJoinBaseSession::IsValidInOuts(CChainState& active_chainstate, const l
if (fConsumeCollateralRet) *fConsumeCollateralRet = true;
return false;
}
// Check for duplicate scripts across all inputs and outputs (privacy requirement)
if (!setScripPubKeys.insert(txout.scriptPubKey).second) {
LogPrint(BCLog::COINJOIN, "CCoinJoinBaseSession::IsValidInOuts -- ERROR: already have this script! scriptPubKey=%s\n", ScriptToAsmStr(txout.scriptPubKey));
nMessageIDRet = ERR_ALREADY_HAVE;
if (fConsumeCollateralRet) *fConsumeCollateralRet = true;
return false;
}
// IsPayToPublicKeyHash() above already checks for scriptPubKey size,
// no need to double-check, hence no usage of ERR_NON_STANDARD_PUBKEY
return true;
};

CAmount nFees{0};

for (const auto& txout : vout) {
if (!checkTxOut(txout)) {
if (!checkTxOut(txout, nExpectedOutputDenom)) {
return false;
}
nFees -= txout.nValue;
Expand All @@ -246,21 +339,26 @@ bool CCoinJoinBaseSession::IsValidInOuts(CChainState& active_chainstate, const l
return false;
}

if (!checkTxOut(coin.out)) {
if (!checkTxOut(coin.out, nExpectedInputDenom)) {
return false;
}

nFees += coin.out.nValue;
}

// The same size and denom for inputs and outputs ensures their total value is also the same,
// no need to double-check. If not, we are doing something wrong, bail out.
// Value sum must match: inputs == outputs (no fees in CoinJoin)
// This holds for standard mixing (same denom) and promotion/demotion (value preserved)
if (nFees != 0) {
LogPrint(BCLog::COINJOIN, "CCoinJoinBaseSession::%s -- ERROR: non-zero fees! fees: %lld\n", __func__, nFees);
nMessageIDRet = ERR_FEES;
return false;
}

LogPrint(BCLog::COINJOIN, "CCoinJoinBaseSession::%s -- Valid %s entry: %d inputs, %d outputs\n",
__func__,
entryType == EntryType::PROMOTION ? "PROMOTION" : (entryType == EntryType::DEMOTION ? "DEMOTION" : "STANDARD"),
vin.size(), vout.size());

return true;
}

Expand Down Expand Up @@ -484,3 +582,96 @@ void CDSTXManager::BlockDisconnected(const std::shared_ptr<const CBlock>& pblock

int CoinJoin::GetMinPoolParticipants() { return Params().PoolMinParticipants(); }
int CoinJoin::GetMaxPoolParticipants() { return Params().PoolMaxParticipants(); }

bool CoinJoin::ValidatePromotionEntry(const std::vector<CTxIn>& vecTxIn, const std::vector<CTxOut>& vecTxOut,
int nSessionDenom, PoolMessage& nMessageIDRet)
{
// Promotion: 10 inputs of smaller denom → 1 output of larger denom
// Session denom is the smaller denom (inputs)
nMessageIDRet = MSG_NOERR;

// Check input count
if (vecTxIn.size() != static_cast<size_t>(PROMOTION_RATIO)) {
LogPrint(BCLog::COINJOIN, "CoinJoin::ValidatePromotionEntry -- ERROR: wrong input count %zu, expected %d\n",
vecTxIn.size(), PROMOTION_RATIO);
nMessageIDRet = ERR_SIZE_MISMATCH;
return false;
}

// Check output count
if (vecTxOut.size() != 1) {
LogPrint(BCLog::COINJOIN, "CoinJoin::ValidatePromotionEntry -- ERROR: wrong output count %zu, expected 1\n",
vecTxOut.size());
nMessageIDRet = ERR_SIZE_MISMATCH;
return false;
}

// Get the larger adjacent denomination
const int nLargerDenom = GetLargerAdjacentDenom(nSessionDenom);
if (nLargerDenom == 0) {
LogPrint(BCLog::COINJOIN, "CoinJoin::ValidatePromotionEntry -- ERROR: no larger adjacent denom for %s\n",
DenominationToString(nSessionDenom));
nMessageIDRet = ERR_DENOM;
return false;
}

// Validate output is at larger denomination
const int nOutputDenom = AmountToDenomination(vecTxOut[0].nValue);
if (nOutputDenom != nLargerDenom) {
LogPrint(BCLog::COINJOIN, "CoinJoin::ValidatePromotionEntry -- ERROR: output denom %s != expected %s\n",
DenominationToString(nOutputDenom), DenominationToString(nLargerDenom));
nMessageIDRet = ERR_DENOM;
return false;
}

// Validate output is P2PKH
if (!vecTxOut[0].scriptPubKey.IsPayToPublicKeyHash()) {
LogPrint(BCLog::COINJOIN, "CoinJoin::ValidatePromotionEntry -- ERROR: output is not P2PKH\n");
nMessageIDRet = ERR_INVALID_SCRIPT;
return false;
}

return true;
}

bool CoinJoin::ValidateDemotionEntry(const std::vector<CTxIn>& vecTxIn, const std::vector<CTxOut>& vecTxOut,
int nSessionDenom, PoolMessage& nMessageIDRet)
{
// Demotion: 1 input of larger denom → 10 outputs of smaller denom
// Session denom is the smaller denom (outputs)
nMessageIDRet = MSG_NOERR;

// Check input count
if (vecTxIn.size() != 1) {
LogPrint(BCLog::COINJOIN, "CoinJoin::ValidateDemotionEntry -- ERROR: wrong input count %zu, expected 1\n",
vecTxIn.size());
nMessageIDRet = ERR_SIZE_MISMATCH;
return false;
}

// Check output count
if (vecTxOut.size() != static_cast<size_t>(PROMOTION_RATIO)) {
LogPrint(BCLog::COINJOIN, "CoinJoin::ValidateDemotionEntry -- ERROR: wrong output count %zu, expected %d\n",
vecTxOut.size(), PROMOTION_RATIO);
nMessageIDRet = ERR_SIZE_MISMATCH;
return false;
}

// Validate all outputs are at session denomination and P2PKH
for (const auto& txout : vecTxOut) {
const int nDenom = AmountToDenomination(txout.nValue);
if (nDenom != nSessionDenom) {
LogPrint(BCLog::COINJOIN, "CoinJoin::ValidateDemotionEntry -- ERROR: output denom %s != session denom %s\n",
DenominationToString(nDenom), DenominationToString(nSessionDenom));
nMessageIDRet = ERR_DENOM;
return false;
}
if (!txout.scriptPubKey.IsPayToPublicKeyHash()) {
LogPrint(BCLog::COINJOIN, "CoinJoin::ValidateDemotionEntry -- ERROR: output is not P2PKH\n");
nMessageIDRet = ERR_INVALID_SCRIPT;
return false;
}
}

return true;
}
Comment on lines +577 to +668
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Suggestion: ValidatePromotionEntry/ValidateDemotionEntry are dead code in production

ValidatePromotionEntry() and ValidateDemotionEntry() are declared in coinjoin.h, implemented in coinjoin.cpp (lines 586-677), and extensively tested in coinjoin_inouts_tests.cpp. However, they are never called from production code — the actual validation is performed inline within IsValidInOuts(). This creates dead code and a maintenance risk: the tested functions could diverge from the actual validation path without detection.

💡 Suggested change
Suggested change
bool CoinJoin::ValidatePromotionEntry(const std::vector<CTxIn>& vecTxIn, const std::vector<CTxOut>& vecTxOut,
int nSessionDenom, PoolMessage& nMessageIDRet)
{
// Promotion: 10 inputs of smaller denom → 1 output of larger denom
// Session denom is the smaller denom (inputs)
nMessageIDRet = MSG_NOERR;
// Check input count
if (vecTxIn.size() != static_cast<size_t>(PROMOTION_RATIO)) {
LogPrint(BCLog::COINJOIN, "CoinJoin::ValidatePromotionEntry -- ERROR: wrong input count %zu, expected %d\n",
vecTxIn.size(), PROMOTION_RATIO);
nMessageIDRet = ERR_SIZE_MISMATCH;
return false;
}
// Check output count
if (vecTxOut.size() != 1) {
LogPrint(BCLog::COINJOIN, "CoinJoin::ValidatePromotionEntry -- ERROR: wrong output count %zu, expected 1\n",
vecTxOut.size());
nMessageIDRet = ERR_SIZE_MISMATCH;
return false;
}
// Get the larger adjacent denomination
const int nLargerDenom = GetLargerAdjacentDenom(nSessionDenom);
if (nLargerDenom == 0) {
LogPrint(BCLog::COINJOIN, "CoinJoin::ValidatePromotionEntry -- ERROR: no larger adjacent denom for %s\n",
DenominationToString(nSessionDenom));
nMessageIDRet = ERR_DENOM;
return false;
}
// Validate output is at larger denomination
const int nOutputDenom = AmountToDenomination(vecTxOut[0].nValue);
if (nOutputDenom != nLargerDenom) {
LogPrint(BCLog::COINJOIN, "CoinJoin::ValidatePromotionEntry -- ERROR: output denom %s != expected %s\n",
DenominationToString(nOutputDenom), DenominationToString(nLargerDenom));
nMessageIDRet = ERR_DENOM;
return false;
}
// Validate output is P2PKH
if (!vecTxOut[0].scriptPubKey.IsPayToPublicKeyHash()) {
LogPrint(BCLog::COINJOIN, "CoinJoin::ValidatePromotionEntry -- ERROR: output is not P2PKH\n");
nMessageIDRet = ERR_INVALID_SCRIPT;
return false;
}
return true;
}
bool CoinJoin::ValidateDemotionEntry(const std::vector<CTxIn>& vecTxIn, const std::vector<CTxOut>& vecTxOut,
int nSessionDenom, PoolMessage& nMessageIDRet)
{
// Demotion: 1 input of larger denom → 10 outputs of smaller denom
// Session denom is the smaller denom (outputs)
nMessageIDRet = MSG_NOERR;
// Check input count
if (vecTxIn.size() != 1) {
LogPrint(BCLog::COINJOIN, "CoinJoin::ValidateDemotionEntry -- ERROR: wrong input count %zu, expected 1\n",
vecTxIn.size());
nMessageIDRet = ERR_SIZE_MISMATCH;
return false;
}
// Check output count
if (vecTxOut.size() != static_cast<size_t>(PROMOTION_RATIO)) {
LogPrint(BCLog::COINJOIN, "CoinJoin::ValidateDemotionEntry -- ERROR: wrong output count %zu, expected %d\n",
vecTxOut.size(), PROMOTION_RATIO);
nMessageIDRet = ERR_SIZE_MISMATCH;
return false;
}
// Validate all outputs are at session denomination and P2PKH
for (const auto& txout : vecTxOut) {
const int nDenom = AmountToDenomination(txout.nValue);
if (nDenom != nSessionDenom) {
LogPrint(BCLog::COINJOIN, "CoinJoin::ValidateDemotionEntry -- ERROR: output denom %s != session denom %s\n",
DenominationToString(nDenom), DenominationToString(nSessionDenom));
nMessageIDRet = ERR_DENOM;
return false;
}
if (!txout.scriptPubKey.IsPayToPublicKeyHash()) {
LogPrint(BCLog::COINJOIN, "CoinJoin::ValidateDemotionEntry -- ERROR: output is not P2PKH\n");
nMessageIDRet = ERR_INVALID_SCRIPT;
return false;
}
}
return true;
}
Either refactor IsValidInOuts() to delegate to these functions, or remove them and test IsValidInOuts() directly.

source: ['claude']

🤖 Fix this with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `src/coinjoin/coinjoin.cpp`:
- [SUGGESTION] lines 586-677: ValidatePromotionEntry/ValidateDemotionEntry are dead code in production
  ValidatePromotionEntry() and ValidateDemotionEntry() are declared in coinjoin.h, implemented in coinjoin.cpp (lines 586-677), and extensively tested in coinjoin_inouts_tests.cpp. However, they are never called from production code — the actual validation is performed inline within IsValidInOuts(). This creates dead code and a maintenance risk: the tested functions could diverge from the actual validation path without detection.

Loading
Loading