Skip to content
Open
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
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)
565 changes: 551 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_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 @@ -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
214 changes: 198 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,71 @@ 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;
}

// Validate promotion/demotion entries using dedicated validators
// and determine expected denominations for UTXO input validation
int nExpectedInputDenom = nSessionDenom;
int nExpectedOutputDenom = nSessionDenom;

if (entryType == EntryType::PROMOTION) {
if (!CoinJoin::ValidatePromotionEntry(vin, vout, nSessionDenom, nMessageIDRet)) {
if (fConsumeCollateralRet) *fConsumeCollateralRet = true;
return false;
}
nExpectedOutputDenom = CoinJoin::GetLargerAdjacentDenom(nSessionDenom);
} else if (entryType == EntryType::DEMOTION) {
if (!CoinJoin::ValidateDemotionEntry(vin, vout, nSessionDenom, nMessageIDRet)) {
if (fConsumeCollateralRet) *fConsumeCollateralRet = true;
return false;
}
nExpectedInputDenom = CoinJoin::GetLargerAdjacentDenom(nSessionDenom);
}
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 +291,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 +330,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 +573,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