ln: require LnFeatures.OPTION_CHANNEL_TYPE as bolts now mandate it
What changed, and why it matters
This commit updates Electrum's Lightning code to follow a newer version of the Lightning Network specification (BOLTs). It makes the 'channel type' feature mandatory instead of optional when opening or accepting Lightning channels. The change removes old conditional code paths and simplifies the logic. There is no direct evidence in the commit that this fixes an active security vulnerability; it appears to be a protocol-compliance and code-simplification change.
Treat as a routine protocol-compliance update. Review whether the mandatory feature requirement could cause interoperability issues with older or non-compliant Lightning peers, but no immediate security response is indicated by the commit itself.
Security signals we found
Protocol compliance update to mandatory Lightning feature
Removal of optional feature negotiation fallback paths
Unconditional validation of channel_type in open_channel and accept_channel
No explicit vulnerability or CVE mentioned in commit message
Evidence from the diff
The patch removes the is_channel_type() helper and all conditional branches that depended on whether the peer advertised OPTION_CHANNEL_TYPE_OPT. It adds LnFeatures.OPTION_CHANNEL_TYPE_REQ to the wallet’s advertised feature set, so Electrum now requires channel_type negotiation. Channel opening and acceptance logic now unconditionally sets and validates channel_type, and passes the ChannelType object around instead of a boolean has_anchors flag. The change follows BOLTs commit 9d456b1c4a6c8e05a6b5b5edbc6c10f7b4b8e4de.
Changed components
electrum/lnpeer.pyelectrum/lnutil.pyelectrum/lnworker.pyInspect captured patch +24 / −29
diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py
index 55a591c..e241e3f 100644
--- a/electrum/lnpeer.py
+++ b/electrum/lnpeer.py
@@ -894,9 +894,6 @@ class Peer(Logger, EventListener):
def is_shutdown_anysegwit(self):
return self.features.supports(LnFeatures.OPTION_SHUTDOWN_ANYSEGWIT_OPT)
- def is_channel_type(self):
- return self.features.supports(LnFeatures.OPTION_CHANNEL_TYPE_OPT)
-
def accepts_zeroconf(self):
return self.features.supports(LnFeatures.OPTION_ZEROCONF_OPT)
@@ -936,10 +933,11 @@ class Peer(Logger, EventListener):
# flexibility to decide an address at closing time
upfront_shutdown_script = b''
- if self.use_anchors():
+ assert channel_type is not None
+ if channel_type & ChannelType.OPTION_ANCHORS_ZERO_FEE_HTLC_TX: # anchors
static_payment_key = self.lnworker.static_payment_key
static_remotekey = None
- else:
+ else: # static_remotekey
assert channel_type & channel_type.OPTION_STATIC_REMOTEKEY
wallet = self.lnworker.wallet
assert wallet.txin_type == 'p2wpkh'
@@ -1055,13 +1053,12 @@ class Peer(Logger, EventListener):
# Eclair accepts channel_type with that bit, but does not require it.
# if option_channel_type is negotiated: MUST set channel_type
- if self.is_channel_type():
- # if it includes channel_type: MUST set it to a defined type representing the type it wants.
- open_channel_tlvs['channel_type'] = {
- 'type': our_channel_type.to_bytes_minimal()
- }
+ # if it includes channel_type: MUST set it to a defined type representing the type it wants.
+ open_channel_tlvs['channel_type'] = {
+ 'type': our_channel_type.to_bytes_minimal()
+ }
- if self.use_anchors():
+ if our_channel_type & ChannelType.OPTION_ANCHORS_ZERO_FEE_HTLC_TX:
multisig_funding_keypair = lnutil.derive_multisig_funding_key_if_we_opened(
funding_root_secret=self.lnworker.funding_root_keypair.privkey,
remote_node_id_or_prefix=self.pubkey,
@@ -1168,7 +1165,7 @@ class Peer(Logger, EventListener):
initial_feerate_per_kw=feerate,
config=self.network.config,
peer_features=self.features,
- has_anchors=self.use_anchors(),
+ channel_type=our_channel_type,
)
# -> funding created
@@ -1285,21 +1282,19 @@ class Peer(Logger, EventListener):
channel_type = open_channel_tlvs.get('channel_type') if open_channel_tlvs else None
# The receiving node MAY fail the channel if:
# option_channel_type was negotiated but the message doesn't include a channel_type
- if self.is_channel_type() and channel_type is None:
+ if channel_type is None:
raise Exception("sender has advertised option_channel_type, but hasn't sent the channel type")
# MUST fail the channel if it supports channel_type,
# channel_type was set, and the type is not suitable.
- elif self.is_channel_type() and channel_type is not None:
+ 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)
- if self.is_channel_type():
- is_zeroconf = bool(channel_type & ChannelType.OPTION_ZEROCONF)
- if is_zeroconf and not self.network.config.ZEROCONF_TRUSTED_NODE.startswith(self.pubkey.hex()):
- raise Exception(f"not accepting zeroconf from node {self.pubkey}")
- else:
- is_zeroconf = False
+ is_zeroconf = bool(channel_type & ChannelType.OPTION_ZEROCONF)
+ if is_zeroconf and not self.network.config.ZEROCONF_TRUSTED_NODE.startswith(self.pubkey.hex()):
+ raise Exception(f"not accepting zeroconf from node {self.pubkey}")
if self.lnworker.has_recoverable_channels() and not is_zeroconf:
# FIXME: we might want to keep the connection open
@@ -1324,7 +1319,7 @@ class Peer(Logger, EventListener):
self.logger.info(f"just-in-time opening fee: {channel_opening_fee} msat")
pass
- if self.use_anchors():
+ if channel_type & ChannelType.OPTION_ANCHORS_ZERO_FEE_HTLC_TX:
multisig_funding_keypair = lnutil.derive_multisig_funding_key_if_they_opened(
funding_root_secret=self.lnworker.funding_root_keypair.privkey,
remote_node_id_or_prefix=self.pubkey,
@@ -1370,7 +1365,7 @@ class Peer(Logger, EventListener):
initial_feerate_per_kw=feerate,
config=self.network.config,
peer_features=self.features,
- has_anchors=self.use_anchors(),
+ channel_type=channel_type,
)
channel_flags = ord(payload['channel_flags'])
@@ -1390,12 +1385,10 @@ class Peer(Logger, EventListener):
'upfront_shutdown_script': {
'shutdown_scriptpubkey': local_config.upfront_shutdown_script
},
+ 'channel_type': {
+ 'type': channel_type.to_bytes_minimal(),
+ },
}
- # The sender: if it sets channel_type: MUST set it to the channel_type from open_channel
- if self.is_channel_type():
- accept_channel_tlvs['channel_type'] = {
- 'type': channel_type.to_bytes_minimal()
- }
self.send_message(
'accept_channel',
diff --git a/electrum/lnutil.py b/electrum/lnutil.py
index 01789ed..ff33f39 100644
--- a/electrum/lnutil.py
+++ b/electrum/lnutil.py
@@ -170,8 +170,9 @@ class ChannelConfig(StoredObject):
initial_feerate_per_kw: int,
config: 'SimpleConfig',
peer_features: 'LnFeatures',
- has_anchors: bool,
+ channel_type: 'ChannelType',
) -> None:
+ has_anchors = bool(channel_type & ChannelType.OPTION_ANCHORS_ZERO_FEE_HTLC_TX)
# first we validate the configs separately
local_config.validate_params(funding_sat=funding_sat, config=config, peer_features=peer_features)
remote_config.validate_params(funding_sat=funding_sat, config=config, peer_features=peer_features)
@@ -1621,7 +1622,7 @@ class ChannelType(IntFlag):
OPTION_SCID_ALIAS = 1 << 46
OPTION_ZEROCONF = 1 << 50
- def discard_unknown_and_check(self):
+ def discard_unknown_and_check(self) -> 'ChannelType':
"""Discards unknown flags and checks flag combination."""
flags = list_enabled_bits(self)
known_channel_types = []
diff --git a/electrum/lnworker.py b/electrum/lnworker.py
index 9fefeb1..dddf535 100644
--- a/electrum/lnworker.py
+++ b/electrum/lnworker.py
@@ -203,6 +203,7 @@ LNWALLET_FEATURES = (
| LnFeatures.OPTION_SHUTDOWN_ANYSEGWIT_OPT
| LnFeatures.OPTION_SCID_ALIAS_OPT
| LnFeatures.OPTION_SUPPORT_LARGE_CHANNEL_OPT
+ | LnFeatures.OPTION_CHANNEL_TYPE_REQ
)
LNGOSSIP_FEATURES = (
Why this scored 24/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.