Skip to content

Commit 311fe44

Browse files
fix(coinjoin): apply a DSSIGNFINALTX message atomically
A DSSIGNFINALTX message can carry several signed inputs, but each was applied through a separate AddScriptSig() call that took and released cs_coinjoin on its own, and the signing-state and participant checks were released before the body was even decoded. At the signing-timeout boundary the scheduler's CheckTimeout() could take the lock between two inputs of the same already-received message, still find the sender's later inputs unsigned, select the sender as a non-signer to charge, and reset the session; the remaining inputs then failed against the cleared pool. Capture the signing session the sender was checked against, then revalidate it after decoding and apply the whole batch under one cs_coinjoin critical section, relaying a rejection from inside the same section if an input fails. AddScriptSig() now requires cs_coinjoin. The lock is released before CheckPool(), which then commits the fully signed session; CheckTimeout() sees IsSignaturesComplete() and defers to it. No cs_main is taken in this section: signature verification already ran under cs_coinjoin inside AddScriptSig(). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent 2d00b22 commit 311fe44

2 files changed

Lines changed: 31 additions & 19 deletions

File tree

‎src/coinjoin/server.cpp‎

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -232,14 +232,14 @@ void CCoinJoinServer::ProcessDSSIGNFINALTX(CNode& peer, CDataStream& vRecv)
232232
// Only accept signatures while we are actually collecting them, and only
233233
// from peers that are active participants in this session. Otherwise a
234234
// stray or unauthenticated peer could abort the session for everyone.
235-
if (nState != POOL_STATE_SIGNING) {
236-
LogPrint(BCLog::COINJOIN, "DSSIGNFINALTX -- wrong state, nState=%d, peer=%d\n",
237-
nState.load(), peer.GetId());
238-
PushStatus(peer, STATUS_REJECTED, ERR_SESSION);
239-
return;
240-
}
235+
int session_id{0};
241236
{
242237
LOCK(cs_coinjoin);
238+
if (nState != POOL_STATE_SIGNING) {
239+
LogPrint(BCLog::COINJOIN, "DSSIGNFINALTX -- wrong state, nState=%d, peer=%d\n", nState.load(), peer.GetId());
240+
PushStatus(peer, STATUS_REJECTED, ERR_SESSION);
241+
return;
242+
}
243243
const bool is_participant = std::ranges::any_of(
244244
vecEntries, [&peer](const auto& entry) { return entry.addr == peer.addr; });
245245
if (!is_participant) {
@@ -248,6 +248,7 @@ void CCoinJoinServer::ProcessDSSIGNFINALTX(CNode& peer, CDataStream& vRecv)
248248
PushStatus(peer, STATUS_REJECTED, ERR_INVALID_INPUT);
249249
return;
250250
}
251+
session_id = nSessionID;
251252
}
252253

253254
const size_t max_txins{CoinJoin::GetMaxPoolInputOutputCount()};
@@ -264,18 +265,30 @@ void CCoinJoinServer::ProcessDSSIGNFINALTX(CNode& peer, CDataStream& vRecv)
264265

265266
LogPrint(BCLog::COINJOIN, "DSSIGNFINALTX -- vecTxIn.size() %s\n", vecTxIn.size());
266267

267-
int nTxInIndex = 0;
268-
int nTxInsCount = static_cast<int>(vecTxIn.size());
269-
270-
for (const auto& txin : vecTxIn) {
271-
nTxInIndex++;
272-
if (!AddScriptSig(txin)) {
273-
LogPrint(BCLog::COINJOIN, "DSSIGNFINALTX -- AddScriptSig() failed at %d/%d, session: %d\n", nTxInIndex, nTxInsCount, nSessionID);
274-
LOCK(cs_coinjoin);
275-
RelayStatus(STATUS_REJECTED);
268+
{
269+
LOCK(cs_coinjoin);
270+
// Apply the whole message in one critical section, against the signing session it was
271+
// checked for above. The lock was released while the body was decoded, and releasing it
272+
// between inputs would let a timeout reset the session halfway through one sender's
273+
// already-received signatures, after charging that sender as a non-signer.
274+
if (nSessionID != session_id || nState != POOL_STATE_SIGNING) {
275+
LogPrint(BCLog::COINJOIN, "DSSIGNFINALTX -- session %d is no longer signing, peer=%d\n", session_id,
276+
peer.GetId());
277+
PushStatus(peer, STATUS_REJECTED, ERR_SESSION);
276278
return;
277279
}
278-
LogPrint(BCLog::COINJOIN, "DSSIGNFINALTX -- AddScriptSig() %d/%d success\n", nTxInIndex, nTxInsCount);
280+
int nTxInIndex = 0;
281+
int nTxInsCount = static_cast<int>(vecTxIn.size());
282+
for (const auto& txin : vecTxIn) {
283+
nTxInIndex++;
284+
if (!AddScriptSig(txin)) {
285+
LogPrint(BCLog::COINJOIN, "DSSIGNFINALTX -- AddScriptSig() failed at %d/%d, session: %d\n", nTxInIndex,
286+
nTxInsCount, session_id);
287+
RelayStatus(STATUS_REJECTED);
288+
return;
289+
}
290+
LogPrint(BCLog::COINJOIN, "DSSIGNFINALTX -- AddScriptSig() %d/%d success\n", nTxInIndex, nTxInsCount);
291+
}
279292
}
280293
// all is good
281294
CheckPool();
@@ -940,10 +953,9 @@ bool CCoinJoinServer::AddEntry(const CCoinJoinEntry& entry, PoolMessage& nMessag
940953

941954
bool CCoinJoinServer::AddScriptSig(const CTxIn& txinNew)
942955
{
943-
AssertLockNotHeld(cs_coinjoin);
956+
AssertLockHeld(cs_coinjoin);
944957
LogPrint(BCLog::COINJOIN, "CCoinJoinServer::AddScriptSig -- scriptSig=%s\n", ScriptToAsmStr(txinNew.scriptSig).substr(0, 24));
945958

946-
LOCK(cs_coinjoin);
947959
for (const auto& entry : vecEntries) {
948960
if (std::ranges::any_of(entry.vecTxDSIn,
949961
[&txinNew](const auto& txdsin) { return txdsin.scriptSig == txinNew.scriptSig; })) {

‎src/coinjoin/server.h‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ class CCoinJoinServer : public CCoinJoinBaseSession, public NetHandler
128128
Mutex cs_check_pool;
129129

130130
/// Add signature to a txin
131-
bool AddScriptSig(const CTxIn& txin) EXCLUSIVE_LOCKS_REQUIRED(!cs_coinjoin);
131+
bool AddScriptSig(const CTxIn& txin) EXCLUSIVE_LOCKS_REQUIRED(cs_coinjoin);
132132

133133
/// Choose one bad actor whose collateral should be consumed, if any.
134134
CTransactionRef SelectCollateralToCharge() const EXCLUSIVE_LOCKS_REQUIRED(cs_coinjoin);

0 commit comments

Comments
 (0)