Merge pull request #10833 from SomberNight/202608_swaps_dm_replies
What changed, and why it matters
This patch fixes a crash in Electrum's submarine swap feature when using Nostr direct messages. If a Nostr server sent the same reply twice, the code tried to complete an already-completed 'future' object, which raises an exception in Python's asyncio. The fix ignores duplicate or unknown replies instead of crashing. A malicious or misbehaving swap server could potentially trigger this to disrupt a user's swap transport/connection.
Apply the patch. Consider adding broader exception handling around future resolution in Nostr transport code, and monitor for other unguarded set_result/set_exception calls in async message handlers.
Security signals we found
asyncio.Future.set_result() called without done() check
unhandled InvalidStateError could crash async transport loop
duplicate Nostr DM reply from server treated as fatal instead of benign
denial-of-service vector against swap client transport
Evidence from the diff
In electrum/submarine_swaps.py, check_direct_messages() processes Nostr DM replies. Previously it called fut.set_result(content) whenever a matching future existed, without checking if it was already done. Calling set_result() on an already-resolved asyncio.Future raises InvalidStateError, which in an async loop could crash the transport task. The patch adds guards: skip if no future exists, skip if future is already done, then set result. This is a robustness fix against duplicate events from the Nostr relay/server.
Changed components
electrum/submarine_swaps.pycheck_direct_messages()Nostr-based submarine swap client transportInspect captured patch +7 / −2
### electrum/submarine_swaps.py
@@ -2095,8 +2095,13 @@ async def check_direct_messages(self):
prev_event_id = content['reply_to']
server_pubkey = event.pubkey
fut = self.dm_replies.get((server_pubkey, prev_event_id))
- if fut:
- fut.set_result(content)
+ if fut is None:
+ self.logger.debug(f"got reply from {server_pubkey=} for unknown {prev_event_id=}")
+ continue
+ if fut.done():
+ self.logger.debug(f"got reply from {server_pubkey=} for already done {prev_event_id=}")
+ continue
+ fut.set_result(content)
elif self.sm.is_server and 'method' in content:
if self._swap_server_requests.full():
self.logger.warning(f"too many swap requests, dropping incoming request: {event.id[:10]}...")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.