Skip to content

Commit 3957e09

Browse files
Merge #7241: test: fix intermittent IS lock timeouts by syncing mempools before waiting
d8ac781 test: fix additional IS lock sync sites that passed nodes=[self.nodes[0]] (UdjinM6) c79c2d9 refactor: improve is-waiting helper by including mempool sync bump of mocktime (Konstantin Akimov) a0e4260 test: address review — fix additional IS lock sync sites and remove dead code (UdjinM6) 62d47f7 test: fix intermittent IS lock timeouts by syncing mempools before waiting (UdjinM6) Pull request description: ## Issue being fixed or feature implemented 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. ## What was done? 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. ## How Has This Been Tested? Run tests, should no longer fail in these parts ## Breaking Changes n/a ## Checklist: - [ ] I have performed a self-review of my own code - [ ] I have commented my code, particularly in hard-to-understand areas - [ ] I have added or updated relevant unit/integration/functional/e2e tests - [ ] I have made corresponding changes to the documentation - [ ] I have assigned this pull request to a milestone _(for repository code-owners and collaborators only)_ ACKs for top commit: PastaPastaPasta: utACK d8ac781 knst: utACK d8ac781 Tree-SHA512: 44201c2afcbea4c0a61e96ad2217c8beae9bc54c80c5c240c74a47af4d533ccf87ec0e2447cc45d9470ec818dcd56595884c686c28f2751ff0ac24c5ca926081
2 parents cd3c3d9 + d8ac781 commit 3957e09

9 files changed

Lines changed: 43 additions & 79 deletions

test/functional/feature_asset_locks.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -391,9 +391,7 @@ def test_asset_unlocks(self, node_wallet, node, pubkey):
391391

392392
assert_equal(node.getmempoolentry(txid)['fees']['base'], Decimal("0.0007"))
393393
is_id = node_wallet.sendtoaddress(node_wallet.getnewaddress(), 1)
394-
self.bump_mocktime(30)
395-
for node in self.nodes:
396-
self.wait_for_instantlock(is_id, node)
394+
self.wait_for_instantlock(is_id)
397395

398396
rawtx = node.getrawtransaction(txid, 1)
399397
rawtx_is = node.getrawtransaction(is_id, 1)

test/functional/feature_llmq_is_cl_conflicts.py

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -99,11 +99,7 @@ def test_chainlock_overrides_islock(self, test_block_conflict, mine_confllicting
9999
rawtx4_txid = self.nodes[0].sendrawtransaction(rawtx4)
100100

101101
# wait for transactions to propagate
102-
self.bump_mocktime(30)
103-
self.sync_mempools()
104-
for node in self.nodes:
105-
self.wait_for_instantlock(rawtx1_txid, node)
106-
self.wait_for_instantlock(rawtx4_txid, node)
102+
self.wait_for_instantlock(rawtx1_txid, rawtx4_txid)
107103

108104
block = create_block_with_mnpayments(self.mninfo, self.nodes[0], [rawtx2_obj])
109105
if test_block_conflict:
@@ -160,10 +156,7 @@ def test_chainlock_overrides_islock(self, test_block_conflict, mine_confllicting
160156
rawtx5 = self.nodes[0].signrawtransactionwithwallet(rawtx5)['hex']
161157
rawtx5_txid = self.nodes[0].sendrawtransaction(rawtx5)
162158
# wait for the transaction to propagate
163-
self.bump_mocktime(30)
164-
self.sync_mempools()
165-
for node in self.nodes:
166-
self.wait_for_instantlock(rawtx5_txid, node)
159+
self.wait_for_instantlock(rawtx5_txid)
167160

168161
if mine_confllicting:
169162
# Lets verify that the ISLOCKs got pruned and conflicting txes were mined but never confirmed
@@ -243,17 +236,15 @@ def test_chainlock_overrides_islock_overrides_nonchainlock(self):
243236
# Should drop tx1 and accept tx2 because there is an isdlock waiting for it
244237
self.nodes[0].sendrawtransaction(rawtx2)
245238
# bump mocktime to force tx relay
246-
self.bump_mocktime(60)
247-
for node in self.nodes:
248-
self.wait_for_instantlock(rawtx2_txid, node)
239+
self.wait_for_instantlock(rawtx2_txid)
249240

250241
# Should not allow competing txes now
251242
assert_raises_rpc_error(-26, "tx-txlock-conflict", self.nodes[0].sendrawtransaction, rawtx1)
252243

253244
islock_tip = self.generate(self.nodes[0], 1)[0]
254245

246+
self.wait_for_instantlock(rawtx2_txid, skip_sync=True)
255247
for node in self.nodes:
256-
self.wait_for_instantlock(rawtx2_txid, node)
257248
assert_equal(node.getrawtransaction(rawtx2_txid, True)['confirmations'], 1)
258249
assert_equal(node.getbestblockhash(), islock_tip)
259250

test/functional/feature_llmq_is_retroactive.py

Lines changed: 12 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -35,16 +35,6 @@ def sleep_and_assert_no_instantlock(self, txid, node, sleep=5):
3535
time.sleep(sleep)
3636
self.assert_no_instantlock(txid, node)
3737

38-
# random delay before tx is actually send by network could take up to 30 seconds
39-
def wait_for_tx(self, txid, node, expected=True, timeout=60):
40-
def check_tx():
41-
try:
42-
return node.getrawtransaction(txid)
43-
except Exception:
44-
return False
45-
if self.wait_until(check_tx, timeout=timeout, do_assert=expected) and not expected:
46-
raise AssertionError("waiting unexpectedly succeeded")
47-
4838
def create_fund_sign_tx(self):
4939
rawtx = self.nodes[0].createrawtransaction([], {self.nodes[0].getnewaddress(): 1})
5040
rawtx = self.nodes[0].fundrawtransaction(rawtx)['hex']
@@ -67,14 +57,15 @@ def run_test(self):
6757
# 3 nodes should be enough to create an IS lock even if nodes 4 and 5 (which have no tx itself)
6858
# are the only "neighbours" in intra-quorum connections for one of them.
6959
self.bump_mocktime(30)
60+
self.sync_mempools(self.nodes[:3])
7061
self.sleep_and_assert_no_instantlock(txid, self.nodes[0])
7162
# Have to disable ChainLocks to avoid signing a block with a "safe" tx too early
7263
self.nodes[0].sporkupdate("SPORK_19_CHAINLOCKS_ENABLED", 4000000000)
7364
self.wait_for_sporks_same()
7465
# We have to wait in order to include tx in block
7566
self.bump_mocktime(10 * 60 + 1)
7667
block = self.generate(self.nodes[0], 1, sync_fun=self.no_op)[0]
77-
self.wait_for_instantlock(txid, self.nodes[0])
68+
self.wait_for_instantlock(txid, nodes=[self.nodes[0]], skip_sync=True)
7869
self.nodes[0].sporkupdate("SPORK_19_CHAINLOCKS_ENABLED", 0)
7970
self.wait_for_sporks_same()
8071
self.wait_for_chainlocked_block_all_nodes(block)
@@ -87,19 +78,17 @@ def run_test(self):
8778
txid = self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), 1)
8879
# 3 nodes should be enough to create an IS lock even if nodes 4 and 5 (which have no tx itself)
8980
# are the only "neighbours" in intra-quorum connections for one of them.
90-
self.bump_mocktime(30)
91-
self.wait_for_instantlock(txid, self.nodes[0])
81+
self.wait_for_instantlock(txid, nodes=self.nodes[:3])
9282
block = self.generate(self.nodes[0], 1, sync_fun=self.no_op)[0]
9383
self.wait_for_chainlocked_block_all_nodes(block)
9484

9585
self.log.info("testing normal signing with partially known TX")
9686
self.isolate_node(3)
9787
txid = self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), 1)
98-
# Make sure nodes 1 and 2 received the TX before we continue,
88+
# Make sure connected nodes received the TX before we continue,
9989
# otherwise it might announce the TX to node 3 when reconnecting
10090
self.bump_mocktime(30)
101-
self.wait_for_tx(txid, self.nodes[1])
102-
self.wait_for_tx(txid, self.nodes[2])
91+
self.sync_mempools(self.nodes[:3])
10392
self.reconnect_isolated_node(3, 0)
10493
# Make sure nodes actually try re-connecting quorum connections
10594
self.bump_mocktime(30)
@@ -110,7 +99,7 @@ def run_test(self):
11099
self.nodes[3].sendrawtransaction(self.nodes[0].getrawtransaction(txid))
111100
# node 3 should vote on a tx now since it became aware of it via sendrawtransaction
112101
# and this should be enough to complete an IS lock
113-
self.wait_for_instantlock(txid, self.nodes[0])
102+
self.wait_for_instantlock(txid, nodes=[self.nodes[0]], skip_sync=True)
114103

115104
self.log.info("testing retroactive signing with unknown TX")
116105
self.isolate_node(3)
@@ -126,11 +115,10 @@ def run_test(self):
126115
self.log.info("testing retroactive signing with partially known TX")
127116
self.isolate_node(3)
128117
txid = self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), 1)
129-
# Make sure nodes 1 and 2 received the TX before we continue,
118+
# Make sure connected nodes received the TX before we continue,
130119
# otherwise it might announce the TX to node 3 when reconnecting
131120
self.bump_mocktime(30)
132-
self.wait_for_tx(txid, self.nodes[1])
133-
self.wait_for_tx(txid, self.nodes[2])
121+
self.sync_mempools(self.nodes[:3])
134122
self.reconnect_isolated_node(3, 0)
135123
# Make sure nodes actually try re-connecting quorum connections
136124
self.bump_mocktime(30)
@@ -159,10 +147,9 @@ def test_session_timeout(self, do_cycle_llmqs):
159147
rawtx_1 = self.create_fund_sign_tx()
160148
txid_single_node = self.nodes[3].sendrawtransaction(rawtx_1)
161149

162-
# Make sure nodes 1 and 2 received the TX before we continue
150+
# Make sure connected nodes received the TX before we continue
163151
self.bump_mocktime(30)
164-
self.wait_for_tx(txid_all_nodes, self.nodes[1])
165-
self.wait_for_tx(txid_all_nodes, self.nodes[2])
152+
self.sync_mempools(self.nodes[:3])
166153
# Make sure signing is done on nodes 1 and 2 (it's async)
167154
time.sleep(5)
168155
# Make the signing session for the IS lock timeout on nodes 1-3
@@ -174,10 +161,9 @@ def test_session_timeout(self, do_cycle_llmqs):
174161
self.wait_for_mnauth(self.nodes[3], 2)
175162

176163
self.nodes[0].sendrawtransaction(rawtx_1)
177-
# Make sure nodes 1 and 2 received the TX
164+
# Make sure connected nodes received the TX
178165
self.bump_mocktime(30)
179-
self.wait_for_tx(txid_single_node, self.nodes[1])
180-
self.wait_for_tx(txid_single_node, self.nodes[2])
166+
self.sync_mempools(self.nodes[:3])
181167
self.bump_mocktime(30)
182168

183169
# Make sure signing is done on nodes 1 and 2 (it's async)

test/functional/feature_llmq_singlenode.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,7 @@ def run_test(self):
106106

107107
self.log.info("Send funds and wait InstantSend lock")
108108
txid = self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), 1)
109-
self.bump_mocktime(30)
110-
self.wait_for_instantlock(txid, self.nodes[0])
109+
self.wait_for_instantlock(txid)
111110

112111
self.log.info("Test various options to sign messages with nodes")
113112
recsig_time = self.mocktime
@@ -184,9 +183,8 @@ def run_test(self):
184183
self.log.info(f"Chainlock on block: {block_hash} is expecting")
185184
self.wait_for_best_chainlock(self.nodes[0], block_hash)
186185
txid = self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), 1)
187-
self.bump_mocktime(30)
188186
self.log.info(f"InstantSend lock on tx: {txid} is expecting")
189-
self.wait_for_instantlock(txid, self.nodes[0])
187+
self.wait_for_instantlock(txid)
190188

191189

192190
if __name__ == '__main__':

test/functional/feature_notifications.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,8 +139,7 @@ def run_test(self):
139139
tx_count = 10
140140
for _ in range(tx_count):
141141
txid = self.nodes[0].sendtoaddress(self.nodes[1].getnewaddress(), 1)
142-
self.bump_mocktime(30)
143-
self.wait_for_instantlock(txid, self.nodes[0])
142+
self.wait_for_instantlock(txid)
144143

145144
# wait at most 10 seconds for expected number of files before reading the content
146145
self.bump_mocktime(30)

test/functional/interface_zmq_dash.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -296,8 +296,7 @@ def test_instantsend_publishers(self):
296296
assert_equal(['None'], self.nodes[0].getislocks([rpc_raw_tx_1['txid']]))
297297
# Send the first transaction and wait for the InstantLock
298298
rpc_raw_tx_1_hash = self.nodes[0].sendrawtransaction(rpc_raw_tx_1['hex'])
299-
self.bump_mocktime(30)
300-
self.wait_for_instantlock(rpc_raw_tx_1_hash, self.nodes[0])
299+
self.wait_for_instantlock(rpc_raw_tx_1_hash)
301300
# Validate hashtxlock
302301
zmq_tx_lock_hash = self.subscribers[ZMQPublisher.hash_tx_lock].receive().read(32).hex()
303302
assert_equal(zmq_tx_lock_hash, rpc_raw_tx_1['txid'])
@@ -349,8 +348,7 @@ def test_instantsend_publishers(self):
349348
pass
350349
# Now send the tx itself
351350
self.test_node.send_tx(from_hex(msg_tx(),rpc_raw_tx_3['hex']))
352-
self.bump_mocktime(30)
353-
self.wait_for_instantlock(rpc_raw_tx_3['txid'], self.nodes[0])
351+
self.wait_for_instantlock(rpc_raw_tx_3['txid'])
354352
# Validate hashtxlock
355353
zmq_tx_lock_hash = self.subscribers[ZMQPublisher.hash_tx_lock].receive().read(32).hex()
356354
assert_equal(zmq_tx_lock_hash, rpc_raw_tx_3['txid'])
@@ -383,8 +381,7 @@ def test_governance_publishers(self):
383381
}
384382
proposal_hex = ''.join(format(x, '02x') for x in json.dumps(proposal_data).encode())
385383
collateral = self.nodes[0].gobject("prepare", "0", proposal_rev, proposal_time, proposal_hex)
386-
self.bump_mocktime(30)
387-
self.wait_for_instantlock(collateral, self.nodes[0])
384+
self.wait_for_instantlock(collateral)
388385
self.generate(self.nodes[0], 6, sync_fun=lambda: self.sync_blocks())
389386
rpc_proposal_hash = self.nodes[0].gobject("submit", "0", proposal_rev, proposal_time, proposal_hex, collateral)
390387
# Validate hashgovernanceobject

test/functional/p2p_instantsend.py

Lines changed: 5 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,7 @@ def test_block_doublespend(self):
4646
# feed the sender with some balance
4747
sender_addr = sender.getnewaddress()
4848
is_id = self.nodes[0].sendtoaddress(sender_addr, 1)
49-
self.bump_mocktime(30)
50-
for node in self.nodes:
51-
self.wait_for_instantlock(is_id, node)
49+
self.wait_for_instantlock(is_id)
5250
self.generate(self.nodes[0], 2)
5351

5452
# create doublespending transaction, but don't relay it
@@ -61,10 +59,7 @@ def test_block_doublespend(self):
6159
# wait for the transaction to propagate
6260
connected_nodes = self.nodes.copy()
6361
del connected_nodes[self.isolated_idx]
64-
self.sync_mempools(connected_nodes)
65-
self.bump_mocktime(30)
66-
for node in connected_nodes:
67-
self.wait_for_instantlock(is_id, node)
62+
self.wait_for_instantlock(is_id, nodes=connected_nodes)
6863
# send doublespend transaction to isolated node
6964
dblspnd_txid = isolated.sendrawtransaction(dblspnd_tx['hex'])
7065
# generate block on isolated node with doublespend transaction
@@ -106,9 +101,7 @@ def test_mempool_doublespend(self):
106101
# feed the sender with some balance
107102
sender_addr = sender.getnewaddress()
108103
is_id = self.nodes[0].sendtoaddress(sender_addr, 1)
109-
self.bump_mocktime(30)
110-
for node in self.nodes:
111-
self.wait_for_instantlock(is_id, node)
104+
self.wait_for_instantlock(is_id)
112105
self.generate(self.nodes[0], 2)
113106

114107
# create doublespending transaction, but don't relay it
@@ -129,17 +122,11 @@ def test_mempool_doublespend(self):
129122
receiver_addr = receiver.getnewaddress()
130123
is_id = sender.sendtoaddress(receiver_addr, 0.9)
131124
# wait for the transaction to propagate
132-
self.bump_mocktime(30)
133-
self.sync_mempools()
134-
for node in self.nodes:
135-
self.wait_for_instantlock(is_id, node)
125+
self.wait_for_instantlock(is_id)
136126
assert dblspnd_txid not in set(isolated.getrawmempool())
137127
# send coins back to the controller node without waiting for confirmations
138128
sentback_id = receiver.sendtoaddress(self.nodes[0].getnewaddress(), 0.9, "", "", True)
139-
self.bump_mocktime(30)
140-
self.sync_mempools()
141-
for node in self.nodes:
142-
self.wait_for_instantlock(sentback_id, node)
129+
self.wait_for_instantlock(sentback_id)
143130
assert_equal(receiver.getwalletinfo()["balance"], 0)
144131
# mine more blocks
145132
self.generate(self.nodes[0], 2)

test/functional/rpc_verifyislock.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,7 @@ def run_test(self):
4141
self.generate(self.nodes[0], 8, sync_fun=self.sync_blocks())
4242

4343
txid = node.sendtoaddress(node.getnewaddress(), 1)
44-
self.bump_mocktime(30)
45-
self.wait_for_instantlock(txid, node)
44+
self.wait_for_instantlock(txid, nodes=[node])
4645

4746
request_id = self.get_request_id(self.nodes[0].getrawtransaction(txid))
4847
request_id_rpc = self.nodes[0].getislocks([txid])[0]["id"]

test/functional/test_framework/test_framework.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1945,17 +1945,26 @@ def create_isdlock(self, hextx):
19451945
return isdlock
19461946

19471947
# due to privacy reasons random delay is used before sending transaction by network
1948-
# most times is just 2-5 seconds, but once in 1000 it's up to 1000 seconds.
1949-
# it's recommended to bump mocktime for 30 seconds before wait_for_instantlock
1950-
def wait_for_instantlock(self, txid, node, timeout=60):
1948+
# 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.
1949+
def wait_for_instantlock(self, *txids, nodes=None, timeout=60, skip_sync=False):
1950+
nodes = nodes or self.nodes
1951+
1952+
if not skip_sync:
1953+
self.bump_mocktime(30)
1954+
self.sync_mempools(nodes)
1955+
1956+
self.log.info(f"Expecting InstantLock for {list(txids)}")
19511957

19521958
def check_instantlock():
19531959
try:
1954-
return node.getrawtransaction(txid, True)["instantlock"]
1960+
return all(
1961+
node.getrawtransaction(txid, True)["instantlock"]
1962+
for txid in txids
1963+
for node in nodes
1964+
)
19551965
except Exception:
19561966
return False
19571967

1958-
self.log.info(f"Expecting InstantLock for {txid}")
19591968
self.wait_until(check_instantlock, timeout=timeout)
19601969

19611970
def wait_for_chainlocked_block(self, node, block_hash, expected=True, timeout=15):

0 commit comments

Comments
 (0)