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
17 changes: 10 additions & 7 deletions src/net_processing.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3583,7 +3583,7 @@ std::pair<bool /*ret*/, bool /*do_return*/> static ValidateDSTX(CDeterministicMN

if (!dmn) {
LogPrint(BCLog::COINJOIN, "DSTX -- Can't find masternode %s to verify %s\n", dstx.masternodeOutpoint.ToStringShort(), hashTx.ToString());
return {false, true};
return {true, true};
}

if (!mn_metaman.IsValidForMixingTxes(dmn->proTxHash)) {
Expand Down Expand Up @@ -4666,12 +4666,15 @@ void PeerManagerImpl::ProcessMessage(

// Process custom logic, no matter if tx will be accepted to mempool later or not
if (nInvType == MSG_DSTX) {
// Validate DSTX and return bRet if we need to return from here
uint256 hashTx = tx.GetHash();
const auto& [bRet, bDoReturn] = ValidateDSTX(*m_dmnman, m_dstxman, m_chainman, m_mn_metaman, m_mempool, dstx, hashTx);
if (bDoReturn) {
return;
}
// Validate DSTX and return bRet if we need to return from here
uint256 hashTx = tx.GetHash();
const auto& [bRet, bDoReturn] = ValidateDSTX(*m_dmnman, m_dstxman, m_chainman, m_mn_metaman, m_mempool, dstx, hashTx);
if (bDoReturn) {
if (!bRet) {
Misbehaving(pfrom.GetId(), 10, "invalid dstx");
Comment on lines +4673 to +4674

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid penalizing unverifiable DSTX relays

When this node is out of IBD but its active tip does not yet include the masternode that signed the DSTX, ValidateDSTX searches only the current tip and 23 ancestors and returns {false, true} from the “Can't find masternode” path. This new !bRet branch therefore increments the peer's misbehavior score for a DSTX that may be valid from the relaying peer's newer chain view, turning a local inability to verify into punishment; consider distinguishing unverifiable/unknown-masternode cases from structural or signature failures before calling Misbehaving.

Useful? React with 👍 / 👎.

}
return;
}
}

LOCK(cs_main);
Expand Down
71 changes: 71 additions & 0 deletions test/functional/p2p_dstx.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
#!/usr/bin/env python3
# Copyright (c) 2026 The Dash Core developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
"""Test P2P CoinJoin broadcast transaction handling."""

import time

from test_framework.messages import (
CCoinJoinBroadcastTx,
COIN,
COutPoint,
CTransaction,
CTxIn,
CTxOut,
msg_dstx,
)
from test_framework.p2p import P2PInterface
from test_framework.script import (
CScript,
OP_CHECKSIG,
OP_DUP,
OP_EQUALVERIFY,
OP_HASH160,
)
from test_framework.test_framework import BitcoinTestFramework


class P2PDSTXTest(BitcoinTestFramework):
def set_test_params(self):
self.num_nodes = 1
self.setup_clean_chain = True
self.extra_args = [["-debug=net"]]

def make_dstx(self):
tx = CTransaction()
tx.vin = [CTxIn(COutPoint(hash=i + 1, n=0)) for i in range(3)]
p2pkh = CScript([
OP_DUP,
OP_HASH160,
b"\x01" * 20,
OP_EQUALVERIFY,
OP_CHECKSIG,
])
tx.vout = [CTxOut(nValue=COIN // 1000 + 1, scriptPubKey=p2pkh) for _ in tx.vin]
return CCoinJoinBroadcastTx(
tx=tx,
m_protxHash=1,
vchSig=b"\x01" * 96,
sigTime=int(time.time()),
)

def run_test(self):
self.log.info("Leave IBD so unsolicited DSTX is processed")
self.generate(self.nodes[0], 1)

peer = self.nodes[0].add_p2p_connection(P2PInterface())

self.log.info("Unknown masternode DSTX is ignored without peer misbehavior")
with self.nodes[0].assert_debug_log([], unexpected_msgs=["Misbehaving", "invalid dstx"]):
peer.send_and_ping(msg_dstx(self.make_dstx()))

Comment thread
thepastaclaw marked this conversation as resolved.
self.log.info("Structurally invalid DSTX is treated as peer misbehavior")
invalid_dstx = self.make_dstx()
invalid_dstx.tx.vout.pop()
with self.nodes[0].assert_debug_log(["Misbehaving", "invalid dstx"]):
peer.send_and_ping(msg_dstx(invalid_dstx))


if __name__ == '__main__':
P2PDSTXTest().main()
47 changes: 47 additions & 0 deletions test/functional/test_framework/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -1835,6 +1835,53 @@ def __repr__(self):
return "msg_tx(tx=%s)" % (repr(self.tx))


class CCoinJoinBroadcastTx:
__slots__ = ("tx", "m_protxHash", "vchSig", "sigTime")

def __init__(self, tx=None, m_protxHash=0, vchSig=b"", sigTime=0):
self.tx = tx or CTransaction()
self.m_protxHash = m_protxHash
self.vchSig = vchSig
self.sigTime = sigTime

def deserialize(self, f):
self.tx = CTransaction()
self.tx.deserialize(f)
self.m_protxHash = deser_uint256(f)
self.vchSig = deser_string(f)
self.sigTime = struct.unpack("<q", f.read(8))[0]

def serialize(self):
r = b""
r += self.tx.serialize()
r += ser_uint256(self.m_protxHash)
r += ser_string(self.vchSig)
r += struct.pack("<q", self.sigTime)
return r

def __repr__(self):
return "CCoinJoinBroadcastTx(tx=%s m_protxHash=%064x)" \
% (repr(self.tx), self.m_protxHash)


class msg_dstx:
__slots__ = ("dstx",)
msgtype = b"dstx"

def __init__(self, dstx=None):
self.dstx = dstx or CCoinJoinBroadcastTx()

def deserialize(self, f):
self.dstx = CCoinJoinBroadcastTx()
self.dstx.deserialize(f)

def serialize(self):
return self.dstx.serialize()

def __repr__(self):
return "msg_dstx(dstx=%s)" % (repr(self.dstx))


class msg_block:
__slots__ = ("block",)
msgtype = b"block"
Expand Down
1 change: 1 addition & 0 deletions test/functional/test_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@
'p2p_nobloomfilter_messages.py',
'p2p_filter.py',
'p2p_blocksonly.py',
'p2p_dstx.py',
'rpc_setban.py --v1transport',
'rpc_setban.py --v2transport',
'mining_prioritisetransaction.py',
Expand Down
Loading