What changed, and why it matters
This commit changes Electrum's Lightning code so that, by default, it refuses to open new 'static_remotekey' channels unless they also use 'anchors'. Static_remotekey is an older channel type. The change is defensive: it prevents users from accidentally opening a less modern channel type, but it does not fix an active bug or vulnerability in existing channels. The commit message and code comments do not describe this as a security fix.
Treat as a defensive hardening change. Review whether your deployment relies on opening new static_remotekey-only Lightning channels; if so, this commit will block them unless anchors are also negotiated. No urgent security patch action is indicated by the supplied materials.
Security signals we found
Hardening: default refusal of new non-anchor static_remotekey Lightning channels
Behavior change in channel-type negotiation
No explicit vulnerability description in commit or references
No patch of an exploitable code path visible in the diff
Evidence from the diff
In electrum/lnpeer.py, the incoming-channel validation now asserts that channel_type includes OPTION_STATIC_REMOTEKEY and, unless TEST_LN_OPEN_SRK_CHANNELS is enabled, also requires OPTION_ANCHORS. The previous TODO comment about failing if channel_type lacks anchors is replaced by an actual check. electrum/lnutil.py’s complies_with_features() is refactored to clarify that channel_type flags must be a subset of negotiated peer features, while peer features can be a superset. Tests are updated accordingly. No CVE, advisory, or vendor security disclosure is present in the supplied materials.
Changed components
electrum/lnpeer.pyelectrum/lnutil.pytests/test_lnutil.pyInspect captured patch +64 / −23
diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py
index 1db7e76..6d8886d 100644
--- a/electrum/lnpeer.py
+++ b/electrum/lnpeer.py
@@ -1243,12 +1243,15 @@ class Peer(Logger, EventListener):
if channel_type is None:
raise Exception("channel_type MUST be present in open_channel, but missing")
# MUST fail the channel if channel_type is not suitable.
- # TODO fail if channel_type does not have anchors
- else:
- channel_type = ChannelType.from_bytes(channel_type['type'], byteorder='big').discard_unknown_and_check()
- if not channel_type.complies_with_features(self.features):
- raise Exception("sender has sent a channel type we don't support")
- assert isinstance(channel_type, ChannelType)
+ channel_type = ChannelType.from_bytes(channel_type['type'], byteorder='big').discard_unknown_and_check()
+ if not channel_type.complies_with_features(self.features):
+ raise Exception("sender has sent a channel type we don't support")
+ assert channel_type & ChannelType.OPTION_STATIC_REMOTEKEY, "new legacy channel?!"
+ if not self.config.TEST_LN_OPEN_SRK_CHANNELS:
+ if not channel_type & ChannelType.OPTION_ANCHORS:
+ # note: BOLT-02 does NOT forbid opening new SRK chan
+ # just because ANCHORS has been negotiated as peer feature
+ raise Exception("refusing to open new static_remotekey channel")
is_zeroconf = bool(channel_type & ChannelType.OPTION_ZEROCONF)
if is_zeroconf and not self.config.ZEROCONF_TRUSTED_NODE.startswith(self.pubkey.hex()):
diff --git a/electrum/lnutil.py b/electrum/lnutil.py
index b49e5bc..962bac3 100644
--- a/electrum/lnutil.py
+++ b/electrum/lnutil.py
@@ -1619,8 +1619,8 @@ class ChannelType(IntFlag):
OPTION_LEGACY_CHANNEL = 0
OPTION_STATIC_REMOTEKEY = 1 << 12
OPTION_ANCHORS = 1 << 22
- OPTION_SCID_ALIAS = 1 << 46
- OPTION_ZEROCONF = 1 << 50
+ OPTION_SCID_ALIAS = 1 << 46 # variation flag
+ OPTION_ZEROCONF = 1 << 50 # variation flag
def discard_unknown_and_check(self) -> 'ChannelType':
"""Discards unknown flags and checks flag combination."""
@@ -1638,6 +1638,7 @@ class ChannelType(IntFlag):
return final_channel_type
def check_combinations(self):
+ """Raises if invalid flag combination."""
basic_type = self & ~(ChannelType.OPTION_SCID_ALIAS | ChannelType.OPTION_ZEROCONF)
if basic_type not in [
ChannelType.OPTION_STATIC_REMOTEKEY,
@@ -1645,13 +1646,22 @@ class ChannelType(IntFlag):
]:
raise ValueError("Channel type is not a valid flag combination.")
- def complies_with_features(self, features: LnFeatures) -> bool:
- flags = list_enabled_bits(self)
- complies = True
- for flag in flags:
- feature = LnFeatures(1 << flag)
- complies &= features.supports(feature)
- return complies
+ def complies_with_features(self, peer_features: LnFeatures) -> bool:
+ """Returns whether channel_type complies with peer_features.
+ peer_features is negotiated features for the peer session.
+
+ note: enforces channel_type is-SUBSET-of peer_features
+ but does NOT enforce cflag-related-peer_features is-SUBSET-of channel_type.
+ For example, even if opt_anchors is a negotiated peer_feature, (as per my reading of BOLT-02),
+ it is still allowed to open an SRK channel (by setting channel_type accordingly).
+ """
+ cflags = list_enabled_bits(self)
+ # channel flags must be a SUBSET of peer_features
+ for cflag in cflags:
+ feature = LnFeatures(1 << cflag)
+ if not peer_features.supports(feature):
+ return False
+ return True
def to_bytes_minimal(self):
# MUST use the smallest bitmap possible to represent the channel type.
diff --git a/tests/test_lnutil.py b/tests/test_lnutil.py
index 944fcc8..cbb53e6 100644
--- a/tests/test_lnutil.py
+++ b/tests/test_lnutil.py
@@ -1054,14 +1054,42 @@ class TestLNUtil(ElectrumTestCase):
None,
LNWallet._decode_channel_update_msg(bytes.fromhex("0101") + msg_without_prefix))
- def test_channel_type(self):
- # test compliance and non compliance with LN features
- features = LnFeatures(LnFeatures.BASIC_MPP_OPT | LnFeatures.OPTION_STATIC_REMOTEKEY_OPT)
- self.assertTrue(ChannelType.OPTION_STATIC_REMOTEKEY.complies_with_features(features))
-
- features = LnFeatures(LnFeatures.BASIC_MPP_OPT | LnFeatures.OPTION_TRAMPOLINE_ROUTING_OPT_ELECTRUM)
- self.assertFalse(ChannelType.OPTION_STATIC_REMOTEKEY.complies_with_features(features))
-
+ def test_channel_type__complies_with_features(self):
+ """test compliance and-non compliance with LN features"""
+ # non-cflag-related peer_features should be ignored (e.g. BASIC_MPP_REQ):
+ pfeatures = LnFeatures(LnFeatures.BASIC_MPP_REQ | LnFeatures.OPTION_STATIC_REMOTEKEY_OPT)
+ ctype = ChannelType.OPTION_STATIC_REMOTEKEY
+ self.assertTrue(ctype.complies_with_features(pfeatures))
+
+ # SRK missing from pfeatures:
+ pfeatures = LnFeatures(LnFeatures.BASIC_MPP_REQ | LnFeatures.OPTION_TRAMPOLINE_ROUTING_OPT_ELECTRUM)
+ ctype = ChannelType.OPTION_STATIC_REMOTEKEY
+ self.assertFalse(ctype.complies_with_features(pfeatures))
+
+ # ANCHORS missing from pfeatures:
+ pfeatures = LnFeatures(LnFeatures.BASIC_MPP_REQ | LnFeatures.OPTION_STATIC_REMOTEKEY_OPT)
+ ctype = ChannelType.OPTION_STATIC_REMOTEKEY | ChannelType.OPTION_ANCHORS
+ self.assertFalse(ctype.complies_with_features(pfeatures))
+
+ # SRK channel_type still allowed, even though ANCHORS has been negotiated:
+ pfeatures = LnFeatures(LnFeatures.BASIC_MPP_REQ | LnFeatures.OPTION_STATIC_REMOTEKEY_OPT | LnFeatures.OPTION_ANCHORS_OPT)
+ ctype = ChannelType.OPTION_STATIC_REMOTEKEY
+ self.assertTrue(ctype.complies_with_features(pfeatures))
+ # same, *even if* ANCHORS is set to REQ. OPT or REQ no longer matters after negotiation finishes:
+ pfeatures = LnFeatures(LnFeatures.BASIC_MPP_REQ | LnFeatures.OPTION_STATIC_REMOTEKEY_OPT | LnFeatures.OPTION_ANCHORS_REQ)
+ ctype = ChannelType.OPTION_STATIC_REMOTEKEY
+ self.assertTrue(ctype.complies_with_features(pfeatures))
+
+ # ANCHORS ctype allowed if pfeatures are correctly negotiated:
+ pfeatures = LnFeatures(LnFeatures.BASIC_MPP_REQ | LnFeatures.OPTION_STATIC_REMOTEKEY_OPT | LnFeatures.OPTION_ANCHORS_OPT)
+ ctype = ChannelType.OPTION_STATIC_REMOTEKEY | ChannelType.OPTION_ANCHORS
+ self.assertTrue(ctype.complies_with_features(pfeatures))
+ # again, OPT or REQ does not matter:
+ pfeatures = LnFeatures(LnFeatures.BASIC_MPP_REQ | LnFeatures.OPTION_STATIC_REMOTEKEY_OPT | LnFeatures.OPTION_ANCHORS_REQ)
+ ctype = ChannelType.OPTION_STATIC_REMOTEKEY | ChannelType.OPTION_ANCHORS
+ self.assertTrue(ctype.complies_with_features(pfeatures))
+
+ def test_channel_type__ignore_unknown(self):
# ignore unknown channel types
channel_type = ChannelType(0b10000000001000000000010).discard_unknown_and_check()
self.assertEqual(ChannelType(0b10000000001000000000000), channel_type)
Why this scored 42/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.