From 3a4fea73605ddf570e079c0f8b013484c55dc729 Mon Sep 17 00:00:00 2001 From: Konstantin Akimov Date: Thu, 30 Apr 2026 15:19:53 +0700 Subject: [PATCH 1/2] test: adds QSENDRECSIGS functional tests --- test/functional/p2p_instantsend.py | 41 ++++++++++++++++++++++ test/functional/test_framework/messages.py | 17 +++++++++ test/functional/test_framework/p2p.py | 3 +- 3 files changed, 60 insertions(+), 1 deletion(-) diff --git a/test/functional/p2p_instantsend.py b/test/functional/p2p_instantsend.py index 8dd14eb35cd7..8ac479ae3d4c 100755 --- a/test/functional/p2p_instantsend.py +++ b/test/functional/p2p_instantsend.py @@ -3,6 +3,8 @@ # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. +from test_framework.messages import msg_qsendrecsigs +from test_framework.p2p import P2PInterface from test_framework.test_framework import DashTestFramework from test_framework.util import assert_equal, assert_raises_rpc_error, force_finish_mnsync @@ -12,6 +14,24 @@ Tests InstantSend functionality (prevent doublespend for unconfirmed transactions) ''' +class RecSigsObserver(P2PInterface): + """Non-MN peer that opts in to recsigs and records every ISDLOCK inv it sees.""" + + def __init__(self): + super().__init__() + self.isdlock_inv_seen = False + + def send_qsendrecsigs(self, on=True): + self.send_message(msg_qsendrecsigs(on)) + + def on_inv(self, message): + for inv in message.inv: + # MSG_ISDLOCK inv type, see src/protocol.h + if inv.type == 31: + self.isdlock_inv_seen = True + super().on_inv(message) + + class InstantSendTest(DashTestFramework): def add_options(self, parser): self.add_wallet_options(parser) @@ -36,6 +56,7 @@ def run_test(self): self.test_mempool_doublespend() self.test_block_doublespend() + self.test_isdlock_relayed_to_recsigs_observer() self.test_instantsend_after_restart() def test_block_doublespend(self): @@ -131,6 +152,26 @@ def test_mempool_doublespend(self): # mine more blocks self.generate(self.nodes[0], 2) + def test_isdlock_relayed_to_recsigs_observer(self): + self.log.info("Non-MN peer started with -watchquorums must still get ISDLOCK invs") + observers = [] + for mn in self.mninfo: + obs = mn.get_node(self).add_p2p_connection(RecSigsObserver()) + obs.send_qsendrecsigs(True) + obs.sync_with_ping() + observers.append(obs) + + txid = self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), 1) + self.wait_for_instantlock(txid) + for obs in observers: + obs.sync_with_ping() + + assert any(o.isdlock_inv_seen for o in observers), \ + "non-MN peer with QSENDRECSIGS got no MSG_ISDLOCK inv" + + # Skip disconnect_p2ps(): the next sub-test "test_instantsend_after_restart()" + # restarts all nodes and tears down these P2P connections anyway. + def test_instantsend_after_restart(self): self.log.info("Testing InstantSend works after full restart without new blocks") diff --git a/test/functional/test_framework/messages.py b/test/functional/test_framework/messages.py index 1bddb79f12f2..eb50edb6badd 100755 --- a/test/functional/test_framework/messages.py +++ b/test/functional/test_framework/messages.py @@ -2447,6 +2447,23 @@ def __repr__(self): return "msg_qsigshare(sigShares=%d)" % (len(self.sig_shares)) +class msg_qsendrecsigs: + __slots__ = ("on",) + msgtype = b"qsendrecsigs" + + def __init__(self, on=True): + self.on = on + + def deserialize(self, f): + self.on = bool(struct.unpack(" Date: Sun, 3 May 2026 21:57:27 +0300 Subject: [PATCH 2/2] test: rename msg_qsendrecsigs.on to wants_recsigs and tear down observers Match the m_wants_recsigs name used in net_processing.cpp, and explicitly disconnect the RecSigsObserver peers instead of relying on the next sub-test's restart to clean them up. +fixed mapping qsendrecsigs to msg_qsendrecsigs Co-Authored-By: Claude Opus 4.7 --- test/functional/p2p_instantsend.py | 17 +++++++++-------- test/functional/test_framework/messages.py | 12 ++++++------ test/functional/test_framework/p2p.py | 1 + 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/test/functional/p2p_instantsend.py b/test/functional/p2p_instantsend.py index 8ac479ae3d4c..7f4066f3e40d 100755 --- a/test/functional/p2p_instantsend.py +++ b/test/functional/p2p_instantsend.py @@ -21,8 +21,8 @@ def __init__(self): super().__init__() self.isdlock_inv_seen = False - def send_qsendrecsigs(self, on=True): - self.send_message(msg_qsendrecsigs(on)) + def send_qsendrecsigs(self, wants_recsigs=True): + self.send_message(msg_qsendrecsigs(wants_recsigs)) def on_inv(self, message): for inv in message.inv: @@ -156,21 +156,22 @@ def test_isdlock_relayed_to_recsigs_observer(self): self.log.info("Non-MN peer started with -watchquorums must still get ISDLOCK invs") observers = [] for mn in self.mninfo: - obs = mn.get_node(self).add_p2p_connection(RecSigsObserver()) + node = mn.get_node(self) + obs = node.add_p2p_connection(RecSigsObserver()) obs.send_qsendrecsigs(True) obs.sync_with_ping() - observers.append(obs) + observers.append((node, obs)) txid = self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), 1) self.wait_for_instantlock(txid) - for obs in observers: + for _, obs in observers: obs.sync_with_ping() - assert any(o.isdlock_inv_seen for o in observers), \ + assert any(obs.isdlock_inv_seen for _, obs in observers), \ "non-MN peer with QSENDRECSIGS got no MSG_ISDLOCK inv" - # Skip disconnect_p2ps(): the next sub-test "test_instantsend_after_restart()" - # restarts all nodes and tears down these P2P connections anyway. + for node, _ in observers: + node.disconnect_p2ps() def test_instantsend_after_restart(self): self.log.info("Testing InstantSend works after full restart without new blocks") diff --git a/test/functional/test_framework/messages.py b/test/functional/test_framework/messages.py index eb50edb6badd..cbc792c9248e 100755 --- a/test/functional/test_framework/messages.py +++ b/test/functional/test_framework/messages.py @@ -2448,20 +2448,20 @@ def __repr__(self): class msg_qsendrecsigs: - __slots__ = ("on",) + __slots__ = ("wants_recsigs",) msgtype = b"qsendrecsigs" - def __init__(self, on=True): - self.on = on + def __init__(self, wants_recsigs=True): + self.wants_recsigs = wants_recsigs def deserialize(self, f): - self.on = bool(struct.unpack("