test_lnchannel: unittest should_be_closed_due_to_expiring_htlcs
What changed, and why it matters
This commit only adds new automated tests for an existing Lightning Network channel safety feature. It does not change any production code, so it cannot introduce a security vulnerability or directly fix one in the shipped software. The tests verify that Electrum will automatically close a Lightning channel when certain time-sensitive payment promises (HTLCs) are about to expire or have expired without proper resolution.
No security action required. Treat as routine test coverage improvement. Reviewers may optionally verify that the tested production method should_be_closed_due_to_expiring_htlcs behaves as expected, but that code is unchanged by this commit.
Security signals we found
Tests exercise existing force-close logic for expiring HTLCs
No changes to production code paths
No changes to cryptographic primitives or network handling
No advisory, CVE, or vendor security disclosure referenced in commit
Evidence from the diff
The diff adds two unit tests in tests/test_lnchannel.py for Channel.should_be_closed_due_to_expiring_htlcs(). One test covers offered HTLCs, checking behavior during the startup grace period and after it expires. The other covers received HTLCs where the preimage has been released, testing the 30-second revack grace period and the force-close path once that grace period passes. The only import change is replacing privkey_to_pubkey with sha256 for test data generation. No production logic is modified.
Changed components
tests/test_lnchannel.pyInspect captured patch +48 / −1
diff --git a/tests/test_lnchannel.py b/tests/test_lnchannel.py
index 972855f..4e7be7d 100644
--- a/tests/test_lnchannel.py
+++ b/tests/test_lnchannel.py
@@ -37,7 +37,7 @@ from electrum import bitcoin
from electrum import lnpeer
from electrum import lnchannel
from electrum import lnutil
-from electrum.crypto import privkey_to_pubkey
+from electrum.crypto import sha256
from electrum.lnutil import (
SENT, LOCAL, REMOTE, RECEIVED, UpdateAddHtlc, LnFeatures, secret_to_pubkey, ChannelType,
effective_htlc_tx_weight, LocalConfig, RemoteConfig, OnlyPubkeyKeypair, ZEROCONF_TIMEOUT,
@@ -848,6 +848,53 @@ class TestChannel(ElectrumTestCase):
self.assertIsNone(self.alice_lnwallet.get_channel_by_id(chan.channel_id))
self.assertIsNone(self.alice_lnwallet.db.get('channels').get(chan.channel_id.hex()))
+ async def test_should_be_closed_due_to_expiring_htlcs_offered_htlcs(self):
+ alice_lnwallet = self.create_mock_lnwallet(name="alice")
+ bob_lnwallet = self.create_mock_lnwallet(name="bob")
+ alice_channel, bob_channel = create_test_channels(alice_lnwallet=alice_lnwallet, bob_lnwallet=bob_lnwallet)
+
+ # no htlcs
+ self.assertFalse(alice_channel.should_be_closed_due_to_expiring_htlcs(local_height=100))
+
+ # one offered htlc, not expired
+ htlc = UpdateAddHtlc(payment_hash=sha256(os.urandom(32)), amount_msat=one_bitcoin_in_msat, cltv_abs=1000)
+ alice_channel.add_htlc(htlc)
+ alice_channel.sign_next_commitment()
+ self.assertFalse(alice_channel.should_be_closed_due_to_expiring_htlcs(local_height=100))
+
+ # expired offered htlc, within startup grace period
+ expired_local_height = 1000 + lnutil.NBLOCK_DEADLINE_DELTA_AFTER_EXPIRY_FOR_OFFERED_HTLCS + 5
+ self.assertFalse(alice_channel.should_be_closed_due_to_expiring_htlcs(expired_local_height))
+
+ # expired offered htlc, past startup grace period
+ alice_lnwallet.instantiation_timestamp -= (lnutil.TIME_FOR_OFFERED_HTLCS_TO_GET_FAILED_OFFCHAIN_ON_RESTART + 10)
+ self.assertTrue(alice_channel.should_be_closed_due_to_expiring_htlcs(expired_local_height))
+
+ async def test_should_be_closed_due_to_expiring_htlcs_received_htlcs(self):
+ alice_lnwallet = self.create_mock_lnwallet(name="alice")
+ bob_lnwallet = self.create_mock_lnwallet(name="bob")
+ alice_channel, bob_channel = create_test_channels(alice_lnwallet=alice_lnwallet, bob_lnwallet=bob_lnwallet)
+
+ preimage = os.urandom(32)
+ htlc = UpdateAddHtlc(payment_hash=sha256(preimage), amount_msat=one_bitcoin_in_msat, cltv_abs=100)
+ expired_height = 100 + lnutil.NBLOCK_DEADLINE_DELTA_BEFORE_EXPIRY_FOR_RECEIVED_HTLCS + 5
+ alice_channel.add_htlc(htlc)
+ bob_htlc_id = bob_channel.receive_htlc(htlc).htlc_id
+ force_state_transition(alice_channel, bob_channel)
+
+ # preimage wasn't released
+ self.assertFalse(bob_channel.should_be_closed_due_to_expiring_htlcs(local_height=expired_height))
+
+ # now the preimage is released
+ bob_channel.settle_htlc(preimage, bob_htlc_id)
+
+ # still in 30s grace period waiting for peers revack
+ self.assertFalse(bob_channel.should_be_closed_due_to_expiring_htlcs(local_height=expired_height))
+
+ # now the settled htlc is past the grace period
+ bob_channel.htlc_settle_time[bob_htlc_id] = int(time.time()) - 60
+ self.assertTrue(bob_channel.should_be_closed_due_to_expiring_htlcs(local_height=expired_height))
+
class TestChannelAnchors(TestChannel):
TEST_ANCHOR_CHANNELS = True
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.