lnpeer: maybe_send_commitment: impl batching updates
What changed, and why it matters
This commit adds a small 50-millisecond delay between Lightning Network commitment signature messages so multiple small updates can be batched into a single signature. It is a performance optimization, not a security fix. The change does not appear to address any known vulnerability.
No security action required. Treat as routine performance improvement.
Security signals we found
No security-relevant keywords in commit title or message
Change is explicitly described as an optimization (batching updates)
No bounds, validation, or cryptographic changes
No incident or disclosure references supplied
Evidence from the diff
In electrum/lnpeer.py, the Peer class now tracks _last_commitsig_sent_time and skips sending a new commitment_signed if less than MIN_TIME_BETWEEN_SENDING_COMMITSIGS (0.05 s) has elapsed since the last one. This implements the batching optimization previously described in a TODO comment. Tests are updated to disable the delay where deterministic behavior is needed.
Changed components
electrum/lnpeer.py: Peer.maybe_send_commitmenttests/test_lnpeer.pyInspect captured patch +13 / −4
diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py
index b976cd8..fc5d99b 100644
--- a/electrum/lnpeer.py
+++ b/electrum/lnpeer.py
@@ -77,6 +77,7 @@ class Peer(Logger, EventListener):
'query_short_channel_ids', 'reply_short_channel_ids', 'reply_short_channel_ids_end')
DELAY_INC_MSG_PROCESSING_SLEEP = 0.01
+ MIN_TIME_BETWEEN_SENDING_COMMITSIGS = 0.05
RECV_GOSSIP_QUEUE_SOFT_MAXSIZE = 2000
RECV_GOSSIP_QUEUE_HARD_MAXSIZE = 5000
@@ -132,6 +133,7 @@ class Peer(Logger, EventListener):
self.register_callbacks()
self._num_gossip_messages_forwarded = 0
self._processed_onion_cache = LRUCache(maxsize=100) # type: LRUCache[bytes, ProcessedOnionPacket]
+ self._last_commitsig_sent_time = time.monotonic()
def send_message(self, message_name: str, **kwargs):
assert util.get_running_loop() == util.get_asyncio_loop(), f"this must be run on the asyncio thread!"
@@ -1870,10 +1872,12 @@ class Peer(Logger, EventListener):
# if there are no changes, we will not (and must not) send a new commitment
if not chan.has_pending_changes(REMOTE):
return False
- # TODO possible optimisation: we could explicitly allow batching updates we send. e.g.:
- # - store timestamp of last "send_commitment" in a field
- # - if prev timestamp is recent, early "return False" here
- # note: no need for a timer to delay "send_commitment", existing htlc_switch polling is sufficient
+ now = time.monotonic()
+ if now - self._last_commitsig_sent_time < self.MIN_TIME_BETWEEN_SENDING_COMMITSIGS:
+ # We recently sent "commitment_signed". Delay sending again, to allow batching updates.
+ # No need to set a timer, htlc_switch polling will call us again.
+ return False
+ self._last_commitsig_sent_time = now
self.logger.info(f'send_commitment. chan {chan.short_channel_id}. ctn: {chan.get_next_ctn(REMOTE)}.')
sig_64, htlc_sigs = chan.sign_next_commitment()
self.send_message("commitment_signed", channel_id=chan.channel_id, signature=sig_64, num_htlcs=len(htlc_sigs), htlc_signature=b"".join(htlc_sigs))
diff --git a/tests/test_lnpeer.py b/tests/test_lnpeer.py
index af0716a..897a4b3 100644
--- a/tests/test_lnpeer.py
+++ b/tests/test_lnpeer.py
@@ -734,6 +734,8 @@ class TestPeerDirect(TestPeer):
# note: we don't start peer.htlc_switch() so that the fake htlcs are left alone.
async def f():
p1, p2, w1, w2 = self.prepare_peers(chan_AB, chan_BA)
+ p1.MIN_TIME_BETWEEN_SENDING_COMMITSIGS = 0
+ p2.MIN_TIME_BETWEEN_SENDING_COMMITSIGS = 0
async with OldTaskGroup() as group:
await group.spawn(p1._message_loop())
await group.spawn(p2._message_loop())
@@ -753,6 +755,8 @@ class TestPeerDirect(TestPeer):
# simulating disconnection. recreate transports.
self.logger.info("simulating disconnection. recreating transports.")
p1, p2, w1, w2 = self.prepare_peers(chan_AB, chan_BA)
+ p1.MIN_TIME_BETWEEN_SENDING_COMMITSIGS = 0
+ p2.MIN_TIME_BETWEEN_SENDING_COMMITSIGS = 0
for chan in (chan_AB, chan_BA):
chan.peer_state = PeerState.DISCONNECTED
async with OldTaskGroup() as group:
@@ -1607,6 +1611,7 @@ class TestPeerDirect(TestPeer):
payment_hash=lnaddr.paymenthash,
min_final_cltv_delta=lnaddr.get_min_final_cltv_delta(),
payment_secret=lnaddr.payment_secret)
+ await p2.received_commitsig_event.wait()
# alice closes
await p1.close_channel(alice_channel.channel_id)
gath.cancel()
Why this scored 18/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.