onion_message: add invoice buffer to payinfo cltv delta
What changed, and why it matters
This commit fixes a small timing buffer in Electrum's Lightning blinded-path payments. When Electrum advertised a hidden payment route, it told senders exactly how many extra blocks to reserve for safety. The problem is that new blocks can be mined while a payment is still traveling through the network. If the advertised safety margin was too tight, a payment could arrive with an expiry that is technically too close to the current block height, causing the payment to be rejected or stuck. The fix adds a 3-block buffer to the advertised safety margin so that normal mining delays don't cause failures.
Apply the patch. Users running Electrum Lightning nodes that create or advertise blinded payment paths should upgrade to avoid payment failures caused by tight CLTV expiry deltas. No immediate emergency response is indicated; this is a robustness fix rather than an active exploit.
Security signals we found
Timing/race condition in payment path expiry
Blinded path payinfo TLV under-reports required CLTV headroom
Potential payment failure or forced retry due to tight expiry delta
Evidence from the diff
In onion_message.py, _get_payinfo_for_blinded_path() builds the payinfo TLV for a blinded payment path. The cltv_expiry_delta previously used sum_cltv_expiry_delta + MIN_FINAL_CLTV_DELTA_ACCEPTED. The patch adds MIN_FINAL_CLTV_DELTA_BUFFER_INVOICE (3 blocks) to that sum. The same adjustment is reflected in the unit test. This gives the recipient extra headroom so that if a sender sets the HTLC expiry to local_height + cltv_expiry_delta and a block is mined during forwarding, the final expiry still satisfies the recipient’s minimum final CLTV delta requirement.
Changed components
electrum/onion_message.pytests/test_onion_message.pyInspect captured patch +6 / −4
diff --git a/electrum/onion_message.py b/electrum/onion_message.py
index 0b30a6a..6f216d5 100644
--- a/electrum/onion_message.py
+++ b/electrum/onion_message.py
@@ -41,7 +41,8 @@ from electrum.lnonion import (get_bolt04_onion_key, OnionPacket, process_onion_p
OnionHopsDataSingle, decrypt_onionmsg_data_tlv, encrypt_onionmsg_data_tlv,
get_shared_secrets_along_route, new_onion_packet, encrypt_hops_recipient_data,
next_blinding_from_shared_secret)
-from electrum.lnutil import LnFeatures, MIN_FINAL_CLTV_DELTA_ACCEPTED, MAXIMUM_REMOTE_TO_SELF_DELAY_ACCEPTED
+from electrum.lnutil import (LnFeatures, MIN_FINAL_CLTV_DELTA_ACCEPTED, MAXIMUM_REMOTE_TO_SELF_DELAY_ACCEPTED,
+ MIN_FINAL_CLTV_DELTA_BUFFER_INVOICE)
from electrum.util import OldTaskGroup, log_exceptions, random_shuffled_copy
@@ -481,7 +482,7 @@ def _get_payinfo_for_blinded_path(chan: 'Channel', lnwallet: 'LNWallet'):
payinfo = {
'fee_base_msat': sum_fee_base_msat,
'fee_proportional_millionths': sum_fee_proportional_millionths,
- 'cltv_expiry_delta': sum_cltv_expiry_delta + MIN_FINAL_CLTV_DELTA_ACCEPTED,
+ 'cltv_expiry_delta': sum_cltv_expiry_delta + MIN_FINAL_CLTV_DELTA_ACCEPTED + MIN_FINAL_CLTV_DELTA_BUFFER_INVOICE,
'htlc_minimum_msat': blinded_path_min_htlc_msat,
'htlc_maximum_msat': blinded_path_max_htlc_msat,
'flen': 0,
diff --git a/tests/test_onion_message.py b/tests/test_onion_message.py
index 8b02d23..8b7d355 100644
--- a/tests/test_onion_message.py
+++ b/tests/test_onion_message.py
@@ -19,7 +19,8 @@ from electrum.lnonion import (
encrypt_hops_recipient_data, blinding_privkey, decrypt_onionmsg_data_tlv)
from electrum.crypto import get_ecdh, privkey_to_pubkey
from electrum.lntransport import LNPeerAddr
-from electrum.lnutil import LnFeatures, Keypair, MIN_FINAL_CLTV_DELTA_ACCEPTED, REMOTE
+from electrum.lnutil import (LnFeatures, Keypair, MIN_FINAL_CLTV_DELTA_ACCEPTED, REMOTE,
+ MIN_FINAL_CLTV_DELTA_BUFFER_INVOICE)
from electrum.onion_message import (
create_blinded_path, OnionMessageManager, NoRouteFound, Timeout,
create_route_to_introduction_point, get_blinded_paths_to_me
@@ -511,7 +512,7 @@ class TestOnionMessageUtils(TestPeer):
self.assertEqual(payinfos[0], {
'fee_base_msat': bob_chan.forwarding_fee_base_msat,
'fee_proportional_millionths': bob_chan.forwarding_fee_proportional_millionths,
- 'cltv_expiry_delta': bob_chan.forwarding_cltv_delta + MIN_FINAL_CLTV_DELTA_ACCEPTED,
+ 'cltv_expiry_delta': bob_chan.forwarding_cltv_delta + MIN_FINAL_CLTV_DELTA_ACCEPTED + MIN_FINAL_CLTV_DELTA_BUFFER_INVOICE,
'htlc_minimum_msat': bob_chan.config[REMOTE].htlc_minimum_msat,
'htlc_maximum_msat': min(bob_chan.config[REMOTE].max_htlc_value_in_flight_msat, 1000 * bob_chan.constraints.capacity),
'flen': 0,
Why this scored 44/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.