lnpeer: check just-in-time channel opening fee
What changed, and why it matters
This commit adds a safety check for Electrum's Lightning 'just-in-time channel opening' feature. When someone opens a payment channel to you on the fly, the remote party can propose an extra opening fee. Previously the code accepted any fee with only a 'todo' comment. Now it rejects the channel if that fee exceeds 10% of the channel's funding amount. Without this check, a malicious or misconfigured Lightning Service Provider (LSP) could potentially charge an unreasonably high opening fee.
Users running Lightning nodes with zeroconf / just-in-time channel opening should upgrade to a version containing this commit. Operators should also monitor LSP fee announcements once fee-discovery support is added, since the current 10% heuristic is a stopgap.
Security signals we found
Previously unvalidated fee field now has an upper-bound check
Replaces a TODO/pass with an explicit rejection
Just-in-time / zeroconf channel opening path is the affected code path
Mitigates potential overcharging by a remote LSP peer
Evidence from the diff
In electrum/lnpeer.py, the handler for incoming channel open messages now validates the channel_opening_fee TLV. It converts the fee from millisatoshis to satoshis and raises an exception if it is greater than 10% of funding_sat. The change replaces a placeholder ‘todo check that the fee is reasonable’ and an unconditional pass with an explicit limit. This is a partial defense because the TODO notes that comparing against advertised LSP fee schedules would be better.
Changed components
electrum/lnpeer.pyLightning peer channel-open handlerJust-in-time (zeroconf) channel openingInspect captured patch +9 / −6
diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py
index fc5d99b..7abe8a0 100644
--- a/electrum/lnpeer.py
+++ b/electrum/lnpeer.py
@@ -1260,13 +1260,16 @@ class Peer(Logger, EventListener):
# store the temp id now, so that it is recognized for e.g. 'error' messages
self.temp_id_to_id[temp_chan_id] = None
self._cleanup_temp_channelids()
- channel_opening_fee_tlv = open_channel_tlvs.get('channel_opening_fee', {})
- channel_opening_fee = channel_opening_fee_tlv.get('channel_opening_fee')
- if channel_opening_fee:
- # todo check that the fee is reasonable
+ channel_opening_fee = open_channel_tlvs.get('channel_opening_fee', {}).get('channel_opening_fee')
+ if channel_opening_fee: # just-in-time channel opening
assert is_zeroconf
- self.logger.info(f"just-in-time opening fee: {channel_opening_fee} msat")
- pass
+ # the opening fee consists of the fee configured by the LSP
+ channel_opening_fee_sat = channel_opening_fee // 1000
+ if channel_opening_fee_sat > funding_sat * 0.1:
+ # TODO: if there will be some discovery channel where LSPs announce their fees
+ # we should compare against the fees they announced here.
+ raise Exception(f"{channel_opening_fee_sat=} exceeding fee limit, rejecting channel ({funding_sat=})")
+ self.logger.info(f"just-in-time channel: {channel_opening_fee_sat=}")
if channel_type & ChannelType.OPTION_ANCHORS_ZERO_FEE_HTLC_TX:
multisig_funding_keypair = lnutil.derive_multisig_funding_key_if_they_opened(
Why this scored 64/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.