tests: test_lnpeer: add test_dont_settle_htlcs
What changed, and why it matters
This commit only adds a new automated test to Electrum's Lightning code. It does not change any production code, so it cannot by itself introduce a security vulnerability or fix one. The test checks that a special internal flag called 'dont_settle_htlcs' correctly prevents Lightning payments from being finalized until the flag is removed, which is used for Just-In-Time channels.
No security action needed. Review as ordinary test coverage.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The commit adds test_dont_settle_htlcs to tests/test_lnpeer.py. It exercises LNWallet.dont_settle_htlcs by registering a payment hash, attempting payment, and verifying that HTLCs are not settled while the hash is in the set. It tests both trampoline and non-trampoline paths, and both success (flag removed, payment completes) and failure (preimage deleted, payment fails) cases. No library/runtime code is modified.
Changed components
tests/test_lnpeer.pyInspect captured patch +70 / −0
diff --git a/tests/test_lnpeer.py b/tests/test_lnpeer.py
index e2597b8..dee627c 100644
--- a/tests/test_lnpeer.py
+++ b/tests/test_lnpeer.py
@@ -1971,6 +1971,76 @@ class TestPeerDirect(TestPeer):
with self.assertRaises(SuccessfulTest):
await f()
+ async def test_dont_settle_htlcs(self):
+ """
+ Test that htlcs registered in LNWallet.dont_settle_htlcs don't get fulfilled if the preimage is available.
+ """
+ async def run_test(test_trampoline, test_failure):
+ alice_channel, bob_channel = create_test_channels()
+ p1, p2, w1, w2, _q1, _q2 = self.prepare_peers(alice_channel, bob_channel)
+ if test_trampoline:
+ await self._activate_trampoline(w1)
+ # declare bob as trampoline node
+ electrum.trampoline._TRAMPOLINE_NODES_UNITTESTS = {
+ 'bob': LNPeerAddr(host="127.0.0.1", port=9735, pubkey=w2.node_keypair.pubkey),
+ }
+
+ preimage = os.urandom(32)
+ lnaddr, pay_req = self.prepare_invoice(
+ w2,
+ payment_preimage=preimage,
+ # use a higher min final cltv delta so we can mine some blocks later
+ min_final_cltv_delta=244,
+ )
+
+ # add payment_hash to dont_settle_htlcs so the htlcs are not getting settled
+ w2.dont_settle_htlcs[pay_req.rhash] = None
+
+ async def pay(lnaddr, pay_req):
+ self.assertEqual(PR_UNPAID, w2.get_payment_status(lnaddr.paymenthash))
+ result, log = await util.wait_for2(w1.pay_invoice(pay_req), timeout=3)
+ if result is True:
+ self.assertNotIn(pay_req.rhash, w2.dont_settle_htlcs)
+ self.assertEqual(PR_PAID, w2.get_payment_status(lnaddr.paymenthash))
+ return PaymentDone()
+ else:
+ self.assertIsNone(w2.get_preimage(lnaddr.paymenthash))
+ return PaymentFailure()
+
+ async def wait_for_htlcs():
+ payment_key = w2._get_payment_key(lnaddr.paymenthash)
+ while payment_key.hex() not in w2.received_mpp_htlcs:
+ await asyncio.sleep(0.05)
+ w2.network.blockchain()._height += 25 # mine some blocks, shouldn't affect anything
+ if test_failure:
+ # delete preimage, this will fail htlcs even if registered in dont_settle_htlcs
+ del w2._preimages[pay_req.rhash]
+ return # pay() should fail now
+ await asyncio.sleep(0.25) # give w2 some time to do mistakes
+ self.assertEqual(w2.received_mpp_htlcs[payment_key.hex()].resolution, RecvMPPResolution.COMPLETE)
+ # remove the payment hash from dont_settle_htlcs so the htlcs can get fulfilled
+ del w2.dont_settle_htlcs[pay_req.rhash]
+
+ async def f():
+ async with OldTaskGroup() as group:
+ await group.spawn(p1._message_loop())
+ await group.spawn(p1.htlc_switch())
+ await group.spawn(p2._message_loop())
+ await group.spawn(p2.htlc_switch())
+ await asyncio.sleep(0.01)
+ invoice_features = lnaddr.get_features()
+ self.assertFalse(invoice_features.supports(LnFeatures.BASIC_MPP_OPT))
+ pay_task = await group.spawn(pay(lnaddr, pay_req))
+ await util.wait_for2(wait_for_htlcs(), timeout=2)
+ raise await pay_task
+
+ await f()
+
+ for test_trampoline in [False, True]:
+ for test_failure in [False, True]:
+ with self.assertRaises(PaymentFailure if test_failure else PaymentDone):
+ await run_test(test_trampoline, test_failure)
+
class TestPeerForwarding(TestPeer):
Why this scored 12/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.