lnpeer: don't save our own channel update as remote upd
What changed, and why it matters
This commit fixes a bug in Electrum's Lightning Network code where, after reconnecting to another node, Electrum could mistakenly treat its own channel settings message as coming from the remote peer. That caused a signature verification failure and an InvalidGossipMsg error, potentially disrupting the Lightning connection or channel state handling. The fix checks who originated the message and ignores it if it came from Electrum itself.
Apply the patch. It is a defensive correctness fix that prevents a denial-of-service/disruption scenario in Lightning peer reestablishment. No immediate incident response is indicated unless users are experiencing InvalidGossipMsg errors during channel reconnects.
Security signals we found
Signature verification failure (InvalidGossipMsg) caused by accepting own gossip as remote
Lightning channel reestablishment message handling bug
Potential channel state desynchronization or connection disruption
No authentication bypass or cryptographic weakness introduced
Evidence from the diff
In electrum/lnpeer.py, maybe_save_remote_update() now inspects the channel_flags direction bit (FLAG_DIRECTION) to determine which endpoint originated a channel_update gossip message. If the originator matches the local node’s public key, the update is logged and dropped instead of being passed to chan.set_remote_update(). Previously, a peer such as CLN could echo Electrum’s own channel_update during reestablishment, causing Electrum to store it as the remote update and then fail signature verification against the peer’s pubkey (InvalidGossipMsg). A regression test was added in tests/test_lnpeer.py.
Changed components
electrum/lnpeer.py: Peer.maybe_save_remote_update()electrum/channel_db.py: FLAG_DIRECTION (newly imported)tests/test_lnpeer.py: regression test test_maybe_save_remote_updateInspect captured patch +34 / −1
diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py
index ea8779a..5e83aff 100644
--- a/electrum/lnpeer.py
+++ b/electrum/lnpeer.py
@@ -55,6 +55,7 @@ from .interface import GracefulDisconnect
from .json_db import StoredDict
from .invoices import PR_PAID
from .fee_policy import FEE_LN_ETA_TARGET, FEERATE_PER_KW_MIN_RELAY_LIGHTNING
+from .channel_db import FLAG_DIRECTION
if TYPE_CHECKING:
from .lnworker import LNGossip, LNWallet
@@ -470,6 +471,12 @@ class Peer(Logger, EventListener):
return
for chan in self.channels.values():
if payload['short_channel_id'] in [chan.short_channel_id, chan.get_local_scid_alias()]:
+ # originator: node_id_1 if the least-significant bit of flags is 0 or node_id_2 otherwise
+ flags = int.from_bytes(payload['channel_flags'], byteorder='big', signed=False)
+ originator = sorted(self.node_ids)[flags & FLAG_DIRECTION]
+ if originator == self.lnworker.node_keypair.pubkey:
+ self.logger.debug(f"peer sent us our own channel update for chan {chan.get_id_for_log()}")
+ return
chan.set_remote_update(payload)
self.logger.info(f"saved remote channel_update gossip msg for chan {chan.get_id_for_log()}")
break
diff --git a/tests/test_lnpeer.py b/tests/test_lnpeer.py
index 06ce9fe..6ba3f75 100644
--- a/tests/test_lnpeer.py
+++ b/tests/test_lnpeer.py
@@ -38,7 +38,7 @@ from electrum.crypto import privkey_to_pubkey
from electrum.lnutil import Keypair, PaymentFailure, LnFeatures, HTLCOwner, PaymentFeeBudget, RECEIVED
from electrum.lnchannel import ChannelState, PeerState, Channel
from electrum.lnrouter import LNPathFinder, PathEdge, LNPathInconsistent
-from electrum.channel_db import ChannelDB
+from electrum.channel_db import ChannelDB, InvalidGossipMsg
from electrum.lnworker import LNWallet, NoPathFound, SentHtlcInfo, PaySession, LNPeerManager
from electrum.lnmsg import encode_msg, decode_msg
from electrum import lnmsg
@@ -607,6 +607,32 @@ class TestPeerUtils(TestPeer):
Peer.decode_short_ids(encoded_unsupported)
self.assertIn("unexpected first byte", str(ctx.exception))
+ async def test_maybe_save_remote_update(self):
+ graph = self.prepare_chans_and_peers_in_graph(self.GRAPH_DEFINITIONS['single_chan'])
+ alice_bob_peer, bob_alice_peer = graph.peers[('alice', 'bob')], graph.peers[('bob', 'alice')]
+ alice_bob_chan, bob_alice_chan = graph.channels[('alice', 'bob')], graph.channels[('bob', 'alice')]
+
+ # prepare channel update from alice
+ alice_to_bob_chan_update = alice_bob_chan.get_outgoing_gossip_channel_update()
+ raw = alice_to_bob_chan_update
+ payload = decode_msg(alice_to_bob_chan_update)[1]
+ payload['raw'] = raw
+
+ # bob should accept the update and save it
+ self.assertIsNone(bob_alice_chan.storage.get('remote_update'))
+ bob_alice_peer.maybe_save_remote_update(payload)
+ self.assertEqual(bob_alice_chan.storage.get('remote_update'), raw.hex())
+
+ # alice shouldn't save her own channel update as remote update
+ self.assertIsNone(alice_bob_chan.storage.get('remote_update'))
+ alice_bob_peer.maybe_save_remote_update(payload)
+ self.assertIsNone(alice_bob_chan.storage.get('remote_update'))
+
+ ChannelDB.verify_channel_update(payload, start_node=bob_alice_peer.pubkey)
+ # trying to verify the sig against the wrong pubkey should fail obviously
+ with self.assertRaises(InvalidGossipMsg):
+ ChannelDB.verify_channel_update(payload, start_node=alice_bob_peer.pubkey)
+
class TestPeerDirect(TestPeer):
Why this scored 34/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.