From 62d47f7476ec5463bd551480cc479c0b9b043442 Mon Sep 17 00:00:00 2001 From: UdjinM6 Date: Sat, 21 Mar 2026 23:41:02 +0300 Subject: [PATCH 1/4] test: fix intermittent IS lock timeouts by syncing mempools before waiting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When mocktime is set, NodeClock::now() returns a frozen value. After bump_mocktime(), the trickle relay timer fires once, then m_next_inv_send_time is set ahead of the frozen clock and never fires again. If a TX inv misses that single relay window, it stays stuck in the sender's mempool indefinitely — regardless of real-time elapsed. Add sync_mempools() after bump_mocktime() in all test paths that send a TX and immediately wait for an IS lock. This ensures all nodes (especially MNs that need to sign) have the TX before polling begins. Co-Authored-By: Claude Opus 4.6 --- test/functional/feature_asset_locks.py | 1 + .../functional/feature_llmq_is_retroactive.py | 22 +++++++++---------- test/functional/feature_notifications.py | 1 + test/functional/p2p_instantsend.py | 2 ++ test/functional/rpc_verifyislock.py | 1 + 5 files changed, 15 insertions(+), 12 deletions(-) diff --git a/test/functional/feature_asset_locks.py b/test/functional/feature_asset_locks.py index 24d9ccf27277..470e0fcf1a15 100755 --- a/test/functional/feature_asset_locks.py +++ b/test/functional/feature_asset_locks.py @@ -392,6 +392,7 @@ def test_asset_unlocks(self, node_wallet, node, pubkey): assert_equal(node.getmempoolentry(txid)['fees']['base'], Decimal("0.0007")) is_id = node_wallet.sendtoaddress(node_wallet.getnewaddress(), 1) self.bump_mocktime(30) + self.sync_mempools() for node in self.nodes: self.wait_for_instantlock(is_id, node) diff --git a/test/functional/feature_llmq_is_retroactive.py b/test/functional/feature_llmq_is_retroactive.py index 563408906ec7..6fe2f1815a7c 100755 --- a/test/functional/feature_llmq_is_retroactive.py +++ b/test/functional/feature_llmq_is_retroactive.py @@ -67,6 +67,7 @@ def run_test(self): # 3 nodes should be enough to create an IS lock even if nodes 4 and 5 (which have no tx itself) # are the only "neighbours" in intra-quorum connections for one of them. self.bump_mocktime(30) + self.sync_mempools(self.nodes[:3]) self.sleep_and_assert_no_instantlock(txid, self.nodes[0]) # Have to disable ChainLocks to avoid signing a block with a "safe" tx too early self.nodes[0].sporkupdate("SPORK_19_CHAINLOCKS_ENABLED", 4000000000) @@ -88,6 +89,7 @@ def run_test(self): # 3 nodes should be enough to create an IS lock even if nodes 4 and 5 (which have no tx itself) # are the only "neighbours" in intra-quorum connections for one of them. self.bump_mocktime(30) + self.sync_mempools(self.nodes[:3]) self.wait_for_instantlock(txid, self.nodes[0]) block = self.generate(self.nodes[0], 1, sync_fun=self.no_op)[0] self.wait_for_chainlocked_block_all_nodes(block) @@ -95,11 +97,10 @@ def run_test(self): self.log.info("testing normal signing with partially known TX") self.isolate_node(3) txid = self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), 1) - # Make sure nodes 1 and 2 received the TX before we continue, + # Make sure connected nodes received the TX before we continue, # otherwise it might announce the TX to node 3 when reconnecting self.bump_mocktime(30) - self.wait_for_tx(txid, self.nodes[1]) - self.wait_for_tx(txid, self.nodes[2]) + self.sync_mempools(self.nodes[:3]) self.reconnect_isolated_node(3, 0) # Make sure nodes actually try re-connecting quorum connections self.bump_mocktime(30) @@ -126,11 +127,10 @@ def run_test(self): self.log.info("testing retroactive signing with partially known TX") self.isolate_node(3) txid = self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), 1) - # Make sure nodes 1 and 2 received the TX before we continue, + # Make sure connected nodes received the TX before we continue, # otherwise it might announce the TX to node 3 when reconnecting self.bump_mocktime(30) - self.wait_for_tx(txid, self.nodes[1]) - self.wait_for_tx(txid, self.nodes[2]) + self.sync_mempools(self.nodes[:3]) self.reconnect_isolated_node(3, 0) # Make sure nodes actually try re-connecting quorum connections self.bump_mocktime(30) @@ -159,10 +159,9 @@ def test_session_timeout(self, do_cycle_llmqs): rawtx_1 = self.create_fund_sign_tx() txid_single_node = self.nodes[3].sendrawtransaction(rawtx_1) - # Make sure nodes 1 and 2 received the TX before we continue + # Make sure connected nodes received the TX before we continue self.bump_mocktime(30) - self.wait_for_tx(txid_all_nodes, self.nodes[1]) - self.wait_for_tx(txid_all_nodes, self.nodes[2]) + self.sync_mempools(self.nodes[:3]) # Make sure signing is done on nodes 1 and 2 (it's async) time.sleep(5) # Make the signing session for the IS lock timeout on nodes 1-3 @@ -174,10 +173,9 @@ def test_session_timeout(self, do_cycle_llmqs): self.wait_for_mnauth(self.nodes[3], 2) self.nodes[0].sendrawtransaction(rawtx_1) - # Make sure nodes 1 and 2 received the TX + # Make sure connected nodes received the TX self.bump_mocktime(30) - self.wait_for_tx(txid_single_node, self.nodes[1]) - self.wait_for_tx(txid_single_node, self.nodes[2]) + self.sync_mempools(self.nodes[:3]) self.bump_mocktime(30) # Make sure signing is done on nodes 1 and 2 (it's async) diff --git a/test/functional/feature_notifications.py b/test/functional/feature_notifications.py index a91d15afe981..f4a015f19dc5 100755 --- a/test/functional/feature_notifications.py +++ b/test/functional/feature_notifications.py @@ -140,6 +140,7 @@ def run_test(self): for _ in range(tx_count): txid = self.nodes[0].sendtoaddress(self.nodes[1].getnewaddress(), 1) self.bump_mocktime(30) + self.sync_mempools() self.wait_for_instantlock(txid, self.nodes[0]) # wait at most 10 seconds for expected number of files before reading the content diff --git a/test/functional/p2p_instantsend.py b/test/functional/p2p_instantsend.py index b92d563f9abc..d6b587071e41 100755 --- a/test/functional/p2p_instantsend.py +++ b/test/functional/p2p_instantsend.py @@ -46,6 +46,7 @@ def test_block_doublespend(self): sender_addr = sender.getnewaddress() is_id = self.nodes[0].sendtoaddress(sender_addr, 1) self.bump_mocktime(30) + self.sync_mempools() for node in self.nodes: self.wait_for_instantlock(is_id, node) self.generate(self.nodes[0], 2) @@ -106,6 +107,7 @@ def test_mempool_doublespend(self): sender_addr = sender.getnewaddress() is_id = self.nodes[0].sendtoaddress(sender_addr, 1) self.bump_mocktime(30) + self.sync_mempools() for node in self.nodes: self.wait_for_instantlock(is_id, node) self.generate(self.nodes[0], 2) diff --git a/test/functional/rpc_verifyislock.py b/test/functional/rpc_verifyislock.py index ee153976b77f..8b6853b68cac 100755 --- a/test/functional/rpc_verifyislock.py +++ b/test/functional/rpc_verifyislock.py @@ -42,6 +42,7 @@ def run_test(self): txid = node.sendtoaddress(node.getnewaddress(), 1) self.bump_mocktime(30) + self.sync_mempools() self.wait_for_instantlock(txid, node) request_id = self.get_request_id(self.nodes[0].getrawtransaction(txid)) From a0e4260f6e7c6e4154fc387dea93e77cc56aa50d Mon Sep 17 00:00:00 2001 From: UdjinM6 Date: Sun, 22 Mar 2026 01:55:27 +0300 Subject: [PATCH 2/4] =?UTF-8?q?test:=20address=20review=20=E2=80=94=20fix?= =?UTF-8?q?=20additional=20IS=20lock=20sync=20sites=20and=20remove=20dead?= =?UTF-8?q?=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add sync_mempools() to two more tests with the same vulnerable pattern: - feature_llmq_is_cl_conflicts.py: no whitelist, waits on all nodes - interface_zmq_dash.py: three locations (low risk due to whitelist, but fixed for consistency) Remove dead wait_for_tx() method from feature_llmq_is_retroactive.py whose call sites were all replaced with sync_mempools in the prior commit. Co-Authored-By: Claude Opus 4.6 --- test/functional/feature_llmq_is_cl_conflicts.py | 1 + test/functional/feature_llmq_is_retroactive.py | 10 ---------- test/functional/interface_zmq_dash.py | 3 +++ 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/test/functional/feature_llmq_is_cl_conflicts.py b/test/functional/feature_llmq_is_cl_conflicts.py index 887231fd4221..2a953a01b80c 100755 --- a/test/functional/feature_llmq_is_cl_conflicts.py +++ b/test/functional/feature_llmq_is_cl_conflicts.py @@ -244,6 +244,7 @@ def test_chainlock_overrides_islock_overrides_nonchainlock(self): self.nodes[0].sendrawtransaction(rawtx2) # bump mocktime to force tx relay self.bump_mocktime(60) + self.sync_mempools() for node in self.nodes: self.wait_for_instantlock(rawtx2_txid, node) diff --git a/test/functional/feature_llmq_is_retroactive.py b/test/functional/feature_llmq_is_retroactive.py index 6fe2f1815a7c..d9e25946fd58 100755 --- a/test/functional/feature_llmq_is_retroactive.py +++ b/test/functional/feature_llmq_is_retroactive.py @@ -35,16 +35,6 @@ def sleep_and_assert_no_instantlock(self, txid, node, sleep=5): time.sleep(sleep) self.assert_no_instantlock(txid, node) - # random delay before tx is actually send by network could take up to 30 seconds - def wait_for_tx(self, txid, node, expected=True, timeout=60): - def check_tx(): - try: - return node.getrawtransaction(txid) - except Exception: - return False - if self.wait_until(check_tx, timeout=timeout, do_assert=expected) and not expected: - raise AssertionError("waiting unexpectedly succeeded") - def create_fund_sign_tx(self): rawtx = self.nodes[0].createrawtransaction([], {self.nodes[0].getnewaddress(): 1}) rawtx = self.nodes[0].fundrawtransaction(rawtx)['hex'] diff --git a/test/functional/interface_zmq_dash.py b/test/functional/interface_zmq_dash.py index f6f42702bc27..d737794399a4 100755 --- a/test/functional/interface_zmq_dash.py +++ b/test/functional/interface_zmq_dash.py @@ -297,6 +297,7 @@ def test_instantsend_publishers(self): # Send the first transaction and wait for the InstantLock rpc_raw_tx_1_hash = self.nodes[0].sendrawtransaction(rpc_raw_tx_1['hex']) self.bump_mocktime(30) + self.sync_mempools() self.wait_for_instantlock(rpc_raw_tx_1_hash, self.nodes[0]) # Validate hashtxlock zmq_tx_lock_hash = self.subscribers[ZMQPublisher.hash_tx_lock].receive().read(32).hex() @@ -350,6 +351,7 @@ def test_instantsend_publishers(self): # Now send the tx itself self.test_node.send_tx(from_hex(msg_tx(),rpc_raw_tx_3['hex'])) self.bump_mocktime(30) + self.sync_mempools() self.wait_for_instantlock(rpc_raw_tx_3['txid'], self.nodes[0]) # Validate hashtxlock zmq_tx_lock_hash = self.subscribers[ZMQPublisher.hash_tx_lock].receive().read(32).hex() @@ -384,6 +386,7 @@ def test_governance_publishers(self): proposal_hex = ''.join(format(x, '02x') for x in json.dumps(proposal_data).encode()) collateral = self.nodes[0].gobject("prepare", "0", proposal_rev, proposal_time, proposal_hex) self.bump_mocktime(30) + self.sync_mempools() self.wait_for_instantlock(collateral, self.nodes[0]) self.generate(self.nodes[0], 6, sync_fun=lambda: self.sync_blocks()) rpc_proposal_hash = self.nodes[0].gobject("submit", "0", proposal_rev, proposal_time, proposal_hex, collateral) From c79c2d981dce9031636c641679985cff4399deac Mon Sep 17 00:00:00 2001 From: Konstantin Akimov Date: Mon, 23 Mar 2026 15:07:33 +0700 Subject: [PATCH 3/4] refactor: improve is-waiting helper by including mempool sync bump of mocktime --- test/functional/feature_asset_locks.py | 5 +--- .../feature_llmq_is_cl_conflicts.py | 18 +++---------- .../functional/feature_llmq_is_retroactive.py | 8 +++--- test/functional/feature_llmq_singlenode.py | 6 ++--- test/functional/feature_notifications.py | 4 +-- test/functional/interface_zmq_dash.py | 12 +++------ test/functional/p2p_instantsend.py | 25 ++++--------------- test/functional/rpc_verifyislock.py | 4 +-- .../test_framework/test_framework.py | 19 ++++++++++---- 9 files changed, 34 insertions(+), 67 deletions(-) diff --git a/test/functional/feature_asset_locks.py b/test/functional/feature_asset_locks.py index 470e0fcf1a15..709cc386e7b5 100755 --- a/test/functional/feature_asset_locks.py +++ b/test/functional/feature_asset_locks.py @@ -391,10 +391,7 @@ def test_asset_unlocks(self, node_wallet, node, pubkey): assert_equal(node.getmempoolentry(txid)['fees']['base'], Decimal("0.0007")) is_id = node_wallet.sendtoaddress(node_wallet.getnewaddress(), 1) - self.bump_mocktime(30) - self.sync_mempools() - for node in self.nodes: - self.wait_for_instantlock(is_id, node) + self.wait_for_instantlock(is_id) rawtx = node.getrawtransaction(txid, 1) rawtx_is = node.getrawtransaction(is_id, 1) diff --git a/test/functional/feature_llmq_is_cl_conflicts.py b/test/functional/feature_llmq_is_cl_conflicts.py index 2a953a01b80c..9fa6222708bf 100755 --- a/test/functional/feature_llmq_is_cl_conflicts.py +++ b/test/functional/feature_llmq_is_cl_conflicts.py @@ -99,11 +99,7 @@ def test_chainlock_overrides_islock(self, test_block_conflict, mine_confllicting rawtx4_txid = self.nodes[0].sendrawtransaction(rawtx4) # wait for transactions to propagate - self.bump_mocktime(30) - self.sync_mempools() - for node in self.nodes: - self.wait_for_instantlock(rawtx1_txid, node) - self.wait_for_instantlock(rawtx4_txid, node) + self.wait_for_instantlock(rawtx1_txid, rawtx4_txid) block = create_block_with_mnpayments(self.mninfo, self.nodes[0], [rawtx2_obj]) if test_block_conflict: @@ -160,10 +156,7 @@ def test_chainlock_overrides_islock(self, test_block_conflict, mine_confllicting rawtx5 = self.nodes[0].signrawtransactionwithwallet(rawtx5)['hex'] rawtx5_txid = self.nodes[0].sendrawtransaction(rawtx5) # wait for the transaction to propagate - self.bump_mocktime(30) - self.sync_mempools() - for node in self.nodes: - self.wait_for_instantlock(rawtx5_txid, node) + self.wait_for_instantlock(rawtx5_txid) if mine_confllicting: # Lets verify that the ISLOCKs got pruned and conflicting txes were mined but never confirmed @@ -243,18 +236,15 @@ def test_chainlock_overrides_islock_overrides_nonchainlock(self): # Should drop tx1 and accept tx2 because there is an isdlock waiting for it self.nodes[0].sendrawtransaction(rawtx2) # bump mocktime to force tx relay - self.bump_mocktime(60) - self.sync_mempools() - for node in self.nodes: - self.wait_for_instantlock(rawtx2_txid, node) + self.wait_for_instantlock(rawtx2_txid) # Should not allow competing txes now assert_raises_rpc_error(-26, "tx-txlock-conflict", self.nodes[0].sendrawtransaction, rawtx1) islock_tip = self.generate(self.nodes[0], 1)[0] + self.wait_for_instantlock(rawtx2_txid, skip_sync=True) for node in self.nodes: - self.wait_for_instantlock(rawtx2_txid, node) assert_equal(node.getrawtransaction(rawtx2_txid, True)['confirmations'], 1) assert_equal(node.getbestblockhash(), islock_tip) diff --git a/test/functional/feature_llmq_is_retroactive.py b/test/functional/feature_llmq_is_retroactive.py index d9e25946fd58..b7497380eebe 100755 --- a/test/functional/feature_llmq_is_retroactive.py +++ b/test/functional/feature_llmq_is_retroactive.py @@ -65,7 +65,7 @@ def run_test(self): # We have to wait in order to include tx in block self.bump_mocktime(10 * 60 + 1) block = self.generate(self.nodes[0], 1, sync_fun=self.no_op)[0] - self.wait_for_instantlock(txid, self.nodes[0]) + self.wait_for_instantlock(txid, nodes=[self.nodes[0]], skip_sync=True) self.nodes[0].sporkupdate("SPORK_19_CHAINLOCKS_ENABLED", 0) self.wait_for_sporks_same() self.wait_for_chainlocked_block_all_nodes(block) @@ -78,9 +78,7 @@ def run_test(self): txid = self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), 1) # 3 nodes should be enough to create an IS lock even if nodes 4 and 5 (which have no tx itself) # are the only "neighbours" in intra-quorum connections for one of them. - self.bump_mocktime(30) - self.sync_mempools(self.nodes[:3]) - self.wait_for_instantlock(txid, self.nodes[0]) + self.wait_for_instantlock(txid, nodes=self.nodes[:3]) block = self.generate(self.nodes[0], 1, sync_fun=self.no_op)[0] self.wait_for_chainlocked_block_all_nodes(block) @@ -101,7 +99,7 @@ def run_test(self): self.nodes[3].sendrawtransaction(self.nodes[0].getrawtransaction(txid)) # node 3 should vote on a tx now since it became aware of it via sendrawtransaction # and this should be enough to complete an IS lock - self.wait_for_instantlock(txid, self.nodes[0]) + self.wait_for_instantlock(txid, nodes=[self.nodes[0]], skip_sync=True) self.log.info("testing retroactive signing with unknown TX") self.isolate_node(3) diff --git a/test/functional/feature_llmq_singlenode.py b/test/functional/feature_llmq_singlenode.py index 8ccd88e7fcfc..0fa67a64fc55 100755 --- a/test/functional/feature_llmq_singlenode.py +++ b/test/functional/feature_llmq_singlenode.py @@ -106,8 +106,7 @@ def run_test(self): self.log.info("Send funds and wait InstantSend lock") txid = self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), 1) - self.bump_mocktime(30) - self.wait_for_instantlock(txid, self.nodes[0]) + self.wait_for_instantlock(txid, nodes=[self.nodes[0]]) self.log.info("Test various options to sign messages with nodes") recsig_time = self.mocktime @@ -184,9 +183,8 @@ def run_test(self): self.log.info(f"Chainlock on block: {block_hash} is expecting") self.wait_for_best_chainlock(self.nodes[0], block_hash) txid = self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), 1) - self.bump_mocktime(30) self.log.info(f"InstantSend lock on tx: {txid} is expecting") - self.wait_for_instantlock(txid, self.nodes[0]) + self.wait_for_instantlock(txid, nodes=[self.nodes[0]]) if __name__ == '__main__': diff --git a/test/functional/feature_notifications.py b/test/functional/feature_notifications.py index f4a015f19dc5..8cc66adc299f 100755 --- a/test/functional/feature_notifications.py +++ b/test/functional/feature_notifications.py @@ -139,9 +139,7 @@ def run_test(self): tx_count = 10 for _ in range(tx_count): txid = self.nodes[0].sendtoaddress(self.nodes[1].getnewaddress(), 1) - self.bump_mocktime(30) - self.sync_mempools() - self.wait_for_instantlock(txid, self.nodes[0]) + self.wait_for_instantlock(txid, nodes=[self.nodes[0]]) # wait at most 10 seconds for expected number of files before reading the content self.bump_mocktime(30) diff --git a/test/functional/interface_zmq_dash.py b/test/functional/interface_zmq_dash.py index d737794399a4..d86afc37968c 100755 --- a/test/functional/interface_zmq_dash.py +++ b/test/functional/interface_zmq_dash.py @@ -296,9 +296,7 @@ def test_instantsend_publishers(self): assert_equal(['None'], self.nodes[0].getislocks([rpc_raw_tx_1['txid']])) # Send the first transaction and wait for the InstantLock rpc_raw_tx_1_hash = self.nodes[0].sendrawtransaction(rpc_raw_tx_1['hex']) - self.bump_mocktime(30) - self.sync_mempools() - self.wait_for_instantlock(rpc_raw_tx_1_hash, self.nodes[0]) + self.wait_for_instantlock(rpc_raw_tx_1_hash) # Validate hashtxlock zmq_tx_lock_hash = self.subscribers[ZMQPublisher.hash_tx_lock].receive().read(32).hex() assert_equal(zmq_tx_lock_hash, rpc_raw_tx_1['txid']) @@ -350,9 +348,7 @@ def test_instantsend_publishers(self): pass # Now send the tx itself self.test_node.send_tx(from_hex(msg_tx(),rpc_raw_tx_3['hex'])) - self.bump_mocktime(30) - self.sync_mempools() - self.wait_for_instantlock(rpc_raw_tx_3['txid'], self.nodes[0]) + self.wait_for_instantlock(rpc_raw_tx_3['txid']) # Validate hashtxlock zmq_tx_lock_hash = self.subscribers[ZMQPublisher.hash_tx_lock].receive().read(32).hex() assert_equal(zmq_tx_lock_hash, rpc_raw_tx_3['txid']) @@ -385,9 +381,7 @@ def test_governance_publishers(self): } proposal_hex = ''.join(format(x, '02x') for x in json.dumps(proposal_data).encode()) collateral = self.nodes[0].gobject("prepare", "0", proposal_rev, proposal_time, proposal_hex) - self.bump_mocktime(30) - self.sync_mempools() - self.wait_for_instantlock(collateral, self.nodes[0]) + self.wait_for_instantlock(collateral, nodes=[self.nodes[0]]) self.generate(self.nodes[0], 6, sync_fun=lambda: self.sync_blocks()) rpc_proposal_hash = self.nodes[0].gobject("submit", "0", proposal_rev, proposal_time, proposal_hex, collateral) # Validate hashgovernanceobject diff --git a/test/functional/p2p_instantsend.py b/test/functional/p2p_instantsend.py index d6b587071e41..b1b15863b4cb 100755 --- a/test/functional/p2p_instantsend.py +++ b/test/functional/p2p_instantsend.py @@ -45,10 +45,7 @@ def test_block_doublespend(self): # feed the sender with some balance sender_addr = sender.getnewaddress() is_id = self.nodes[0].sendtoaddress(sender_addr, 1) - self.bump_mocktime(30) - self.sync_mempools() - for node in self.nodes: - self.wait_for_instantlock(is_id, node) + self.wait_for_instantlock(is_id) self.generate(self.nodes[0], 2) # create doublespending transaction, but don't relay it @@ -61,10 +58,7 @@ def test_block_doublespend(self): # wait for the transaction to propagate connected_nodes = self.nodes.copy() del connected_nodes[self.isolated_idx] - self.sync_mempools(connected_nodes) - self.bump_mocktime(30) - for node in connected_nodes: - self.wait_for_instantlock(is_id, node) + self.wait_for_instantlock(is_id, nodes=connected_nodes) # send doublespend transaction to isolated node dblspnd_txid = isolated.sendrawtransaction(dblspnd_tx['hex']) # generate block on isolated node with doublespend transaction @@ -106,10 +100,7 @@ def test_mempool_doublespend(self): # feed the sender with some balance sender_addr = sender.getnewaddress() is_id = self.nodes[0].sendtoaddress(sender_addr, 1) - self.bump_mocktime(30) - self.sync_mempools() - for node in self.nodes: - self.wait_for_instantlock(is_id, node) + self.wait_for_instantlock(is_id) self.generate(self.nodes[0], 2) # create doublespending transaction, but don't relay it @@ -130,17 +121,11 @@ def test_mempool_doublespend(self): receiver_addr = receiver.getnewaddress() is_id = sender.sendtoaddress(receiver_addr, 0.9) # wait for the transaction to propagate - self.bump_mocktime(30) - self.sync_mempools() - for node in self.nodes: - self.wait_for_instantlock(is_id, node) + self.wait_for_instantlock(is_id) assert dblspnd_txid not in set(isolated.getrawmempool()) # send coins back to the controller node without waiting for confirmations sentback_id = receiver.sendtoaddress(self.nodes[0].getnewaddress(), 0.9, "", "", True) - self.bump_mocktime(30) - self.sync_mempools() - for node in self.nodes: - self.wait_for_instantlock(sentback_id, node) + self.wait_for_instantlock(sentback_id) assert_equal(receiver.getwalletinfo()["balance"], 0) # mine more blocks self.generate(self.nodes[0], 2) diff --git a/test/functional/rpc_verifyislock.py b/test/functional/rpc_verifyislock.py index 8b6853b68cac..40aff04c162b 100755 --- a/test/functional/rpc_verifyislock.py +++ b/test/functional/rpc_verifyislock.py @@ -41,9 +41,7 @@ def run_test(self): self.generate(self.nodes[0], 8, sync_fun=self.sync_blocks()) txid = node.sendtoaddress(node.getnewaddress(), 1) - self.bump_mocktime(30) - self.sync_mempools() - self.wait_for_instantlock(txid, node) + self.wait_for_instantlock(txid, nodes=[node]) request_id = self.get_request_id(self.nodes[0].getrawtransaction(txid)) request_id_rpc = self.nodes[0].getislocks([txid])[0]["id"] diff --git a/test/functional/test_framework/test_framework.py b/test/functional/test_framework/test_framework.py index a10d04f0312d..de0e4ca70501 100755 --- a/test/functional/test_framework/test_framework.py +++ b/test/functional/test_framework/test_framework.py @@ -1945,17 +1945,26 @@ def create_isdlock(self, hextx): return isdlock # due to privacy reasons random delay is used before sending transaction by network - # most times is just 2-5 seconds, but once in 1000 it's up to 1000 seconds. - # it's recommended to bump mocktime for 30 seconds before wait_for_instantlock - def wait_for_instantlock(self, txid, node, timeout=60): + # most times it's just 2-5 seconds, but once in 1000 times it's up to 10 seconds and has no bound from the top. + def wait_for_instantlock(self, *txids, nodes=None, timeout=60, skip_sync=False): + nodes = nodes or self.nodes + + if not skip_sync: + self.bump_mocktime(30) + self.sync_mempools(nodes) + + self.log.info(f"Expecting InstantLock for {list(txids)}") def check_instantlock(): try: - return node.getrawtransaction(txid, True)["instantlock"] + return all( + node.getrawtransaction(txid, True)["instantlock"] + for txid in txids + for node in nodes + ) except Exception: return False - self.log.info(f"Expecting InstantLock for {txid}") self.wait_until(check_instantlock, timeout=timeout) def wait_for_chainlocked_block(self, node, block_hash, expected=True, timeout=15): From d8ac7815e3b29334d3f5da5b3acf9c447385f384 Mon Sep 17 00:00:00 2001 From: UdjinM6 Date: Mon, 23 Mar 2026 13:48:13 +0300 Subject: [PATCH 4/4] test: fix additional IS lock sync sites that passed nodes=[self.nodes[0]] wait_for_instantlock(nodes=[self.nodes[0]]) makes the internal sync_mempools a no-op since it only syncs the sender with itself. The MN signer nodes still need the TX, so drop the nodes= restriction to sync all nodes including dynamically added masternodes. Fixes: feature_llmq_singlenode.py (2 sites), feature_notifications.py, interface_zmq_dash.py (governance collateral) Co-Authored-By: Claude Opus 4.6 --- test/functional/feature_llmq_singlenode.py | 4 ++-- test/functional/feature_notifications.py | 2 +- test/functional/interface_zmq_dash.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/test/functional/feature_llmq_singlenode.py b/test/functional/feature_llmq_singlenode.py index 0fa67a64fc55..8e0ba2b1209b 100755 --- a/test/functional/feature_llmq_singlenode.py +++ b/test/functional/feature_llmq_singlenode.py @@ -106,7 +106,7 @@ def run_test(self): self.log.info("Send funds and wait InstantSend lock") txid = self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), 1) - self.wait_for_instantlock(txid, nodes=[self.nodes[0]]) + self.wait_for_instantlock(txid) self.log.info("Test various options to sign messages with nodes") recsig_time = self.mocktime @@ -184,7 +184,7 @@ def run_test(self): self.wait_for_best_chainlock(self.nodes[0], block_hash) txid = self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), 1) self.log.info(f"InstantSend lock on tx: {txid} is expecting") - self.wait_for_instantlock(txid, nodes=[self.nodes[0]]) + self.wait_for_instantlock(txid) if __name__ == '__main__': diff --git a/test/functional/feature_notifications.py b/test/functional/feature_notifications.py index 8cc66adc299f..c0fd38ddcfad 100755 --- a/test/functional/feature_notifications.py +++ b/test/functional/feature_notifications.py @@ -139,7 +139,7 @@ def run_test(self): tx_count = 10 for _ in range(tx_count): txid = self.nodes[0].sendtoaddress(self.nodes[1].getnewaddress(), 1) - self.wait_for_instantlock(txid, nodes=[self.nodes[0]]) + self.wait_for_instantlock(txid) # wait at most 10 seconds for expected number of files before reading the content self.bump_mocktime(30) diff --git a/test/functional/interface_zmq_dash.py b/test/functional/interface_zmq_dash.py index d86afc37968c..212d6002ea1d 100755 --- a/test/functional/interface_zmq_dash.py +++ b/test/functional/interface_zmq_dash.py @@ -381,7 +381,7 @@ def test_governance_publishers(self): } proposal_hex = ''.join(format(x, '02x') for x in json.dumps(proposal_data).encode()) collateral = self.nodes[0].gobject("prepare", "0", proposal_rev, proposal_time, proposal_hex) - self.wait_for_instantlock(collateral, nodes=[self.nodes[0]]) + self.wait_for_instantlock(collateral) self.generate(self.nodes[0], 6, sync_fun=lambda: self.sync_blocks()) rpc_proposal_hash = self.nodes[0].gobject("submit", "0", proposal_rev, proposal_time, proposal_hex, collateral) # Validate hashgovernanceobject