What changed, and why it matters
This commit adds a rate limit to how many unsolicited or rapid 'reply_channel_range' messages Electrum will accept from a Lightning peer. Previously, the queue that stores these replies was always created when the peer object was set up, so a remote peer could keep sending replies even when Electrum never asked for them, potentially filling memory or causing the program to fall behind. Now the queue is only created while Electrum is actively waiting for a reply, and if too many replies pile up the code slows the peer down. This looks like a hardening fix against a denial-of-service or memory-pressure issue, but the commit message does not call it a security fix.
Treat as a low-to-moderate hardening fix. Users running Lightning-enabled Electrum should update to a version containing this commit. Review whether other gossip reply queues have similar lifecycle or rate-limit gaps.
Security signals we found
Rate-limiting added to a peer-triggered message handler
Queue lifecycle tightened to match active request window
Unsolicited replies now rejected explicitly
Potential denial-of-service / memory-pressure hardening
Evidence from the diff
The patch changes lnpeer.Peer so that self.reply_channel_range is None until get_channel_range() is called, and is reset to None after the expected replies are consumed. on_reply_channel_range is made async and now rejects replies when no query is in flight, and sleeps if the queue grows beyond 10 entries. This prevents a peer from driving unbounded queue growth via reply_channel_range messages outside of an active query window. The change is defensive and mitigates a likely DoS vector, but the diff alone does not prove an exploitable crash or memory exhaustion path.
Changed components
electrum/lnpeer.pyLightning peer gossip/channel-range synchronizationInspect captured patch +10 / −2
diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py
index 216bdd8..1b10d2c 100644
--- a/electrum/lnpeer.py
+++ b/electrum/lnpeer.py
@@ -106,7 +106,7 @@ class Peer(Logger, EventListener):
assert self.node_ids[0] != self.node_ids[1]
self.last_message_time = 0
self.pong_event = asyncio.Event()
- self.reply_channel_range = asyncio.Queue()
+ self.reply_channel_range = None # type: Optional[asyncio.Queue]
# gossip uses a single queue to preserve message order
self.recv_gossip_queue = asyncio.Queue(maxsize=self.RECV_GOSSIP_QUEUE_HARD_MAXSIZE)
self.our_gossip_timestamp_filter = None # type: Optional[GossipTimestampFilter]
@@ -709,6 +709,7 @@ class Peer(Logger, EventListener):
self.outgoing_gossip_reply = False
async def get_channel_range(self):
+ self.reply_channel_range = asyncio.Queue()
first_block = constants.net.BLOCK_HEIGHT_FIRST_LIGHTNING_CHANNELS
num_blocks = self.lnworker.network.get_local_height() - first_block
self.query_channel_range(first_block, num_blocks)
@@ -747,6 +748,7 @@ class Peer(Logger, EventListener):
a, b = intervals[0]
if a <= first_block and b >= first_block + num_blocks:
break
+ self.reply_channel_range = None
return ids, complete
def request_gossip(self, timestamp=0):
@@ -784,7 +786,7 @@ class Peer(Logger, EventListener):
ids = [decoded[i:i+8] for i in range(0, len(decoded), 8)]
return ids
- def on_reply_channel_range(self, payload):
+ async def on_reply_channel_range(self, payload):
first = payload['first_blocknum']
num = payload['number_of_blocks']
complete = bool(int.from_bytes(payload['sync_complete'], 'big'))
@@ -792,6 +794,12 @@ class Peer(Logger, EventListener):
ids = self.decode_short_ids(encoded)
# self.logger.info(f"on_reply_channel_range. >>> first_block {first}, num_blocks {num}, "
# f"num_ids {len(ids)}, complete {complete}")
+ if self.reply_channel_range is None:
+ raise Exception("received 'reply_channel_range' without corresponding 'query_channel_range'")
+ while self.reply_channel_range.qsize() > 10:
+ # we block process_message until the queue gets consumed
+ self.logger.info("reply_channel_range queue is overflowing. sleeping...")
+ await asyncio.sleep(0.1)
self.reply_channel_range.put_nowait((first, num, complete, ids))
async def _send_reply_short_channel_ids(self, payload: dict):
Why this scored 46/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.