What changed, and why it matters
This commit adds a new placeholder field called hold_htlc to the Lightning update_add_htlc message structure. The field is never actually set to a value anywhere in the code; it is always initialized to None. It prepares the codebase for a future feature that lets an often-offline sender ask a forwarding node to hold an HTLC until the recipient comes back online. Because the flag is never enabled, this change by itself does not create a practical security vulnerability.
No immediate security action required. Treat as a protocol groundwork commit. Monitor the follow-up work that actually sets hold_htlc and implements ReleaseHeldHtlc handling, since that will be the point at which safety properties (e.g., timeout handling, preimage release, channel reserve accounting) need careful review.
Security signals we found
New optional TLV field added to a core Lightning protocol message (update_add_htlc)
Field is currently always None and no code path sets it
Temporary TLV type (75537) chosen instead of the final BOLTs PR 989 type due to downgrade-read concerns
Comment explicitly states potential failure to read ChannelManager after downgrade if a new even TLV is written
Evidence from the diff
The patch introduces an optional TLV field hold_htlc (type 75537) to msgs::UpdateAddHTLC and wires it through serialization, deserialization, and test fixtures. The field is typed as Option<()> and is always set to None in production code (channel.rs) and in all tests. A code comment notes that the chosen TLV type 75537 is temporary and will be updated later to match BOLTs PR 989 once a downgrade-read issue with new even TLVs is fixed. No logic consumes or acts upon this field.
Changed components
lightning/src/ln/msgs.rs (UpdateAddHTLC struct and serialization)lightning/src/ln/channel.rs (HTLC retransmission)Test files: blinded_payment_tests.rs, functional_tests.rs, htlc_reserve_unit_tests.rs, onion_payment.rs, payment_tests.rsInspect captured patch +20 / −1
diff --git a/lightning/src/ln/blinded_payment_tests.rs b/lightning/src/ln/blinded_payment_tests.rs
index a8e7af2..25fa5e7 100644
--- a/lightning/src/ln/blinded_payment_tests.rs
+++ b/lightning/src/ln/blinded_payment_tests.rs
@@ -1522,6 +1522,7 @@ fn update_add_msg(
onion_routing_packet,
skimmed_fee_msat: None,
blinding_point,
+ hold_htlc: None,
}
}
diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs
index 1065803..2a95e4f 100644
--- a/lightning/src/ln/channel.rs
+++ b/lightning/src/ln/channel.rs
@@ -9045,6 +9045,7 @@ where
onion_routing_packet: (**onion_packet).clone(),
skimmed_fee_msat: htlc.skimmed_fee_msat,
blinding_point: htlc.blinding_point,
+ hold_htlc: None, // Will be set by the async sender when support is added
});
}
}
diff --git a/lightning/src/ln/functional_tests.rs b/lightning/src/ln/functional_tests.rs
index 03fd816..d398d59 100644
--- a/lightning/src/ln/functional_tests.rs
+++ b/lightning/src/ln/functional_tests.rs
@@ -2288,6 +2288,7 @@ pub fn fail_backward_pending_htlc_upon_channel_failure() {
onion_routing_packet,
skimmed_fee_msat: None,
blinding_point: None,
+ hold_htlc: None,
};
nodes[0].node.handle_update_add_htlc(node_b_id, &update_add_htlc);
}
diff --git a/lightning/src/ln/htlc_reserve_unit_tests.rs b/lightning/src/ln/htlc_reserve_unit_tests.rs
index f90b8b8..7f411a6 100644
--- a/lightning/src/ln/htlc_reserve_unit_tests.rs
+++ b/lightning/src/ln/htlc_reserve_unit_tests.rs
@@ -835,6 +835,7 @@ pub fn do_test_fee_spike_buffer(cfg: Option<UserConfig>, htlc_fails: bool) {
onion_routing_packet: onion_packet,
skimmed_fee_msat: None,
blinding_point: None,
+ hold_htlc: None,
};
nodes[1].node.handle_update_add_htlc(node_a_id, &msg);
@@ -1072,6 +1073,7 @@ pub fn test_chan_reserve_violation_inbound_htlc_outbound_channel() {
onion_routing_packet: onion_packet,
skimmed_fee_msat: None,
blinding_point: None,
+ hold_htlc: None,
};
nodes[0].node.handle_update_add_htlc(node_b_id, &msg);
@@ -1255,6 +1257,7 @@ pub fn test_chan_reserve_violation_inbound_htlc_inbound_chan() {
onion_routing_packet: onion_packet,
skimmed_fee_msat: None,
blinding_point: None,
+ hold_htlc: None,
};
nodes[1].node.handle_update_add_htlc(node_a_id, &msg);
@@ -1637,6 +1640,7 @@ pub fn test_update_add_htlc_bolt2_receiver_check_max_htlc_limit() {
onion_routing_packet: onion_packet.clone(),
skimmed_fee_msat: None,
blinding_point: None,
+ hold_htlc: None,
};
for i in 0..50 {
@@ -2242,6 +2246,7 @@ pub fn do_test_dust_limit_fee_accounting(can_afford: bool) {
onion_routing_packet,
skimmed_fee_msat: None,
blinding_point: None,
+ hold_htlc: None,
};
nodes[1].node.handle_update_add_htlc(node_a_id, &msg);
diff --git a/lightning/src/ln/msgs.rs b/lightning/src/ln/msgs.rs
index 0a6817e..ef7e230 100644
--- a/lightning/src/ln/msgs.rs
+++ b/lightning/src/ln/msgs.rs
@@ -765,6 +765,11 @@ pub struct UpdateAddHTLC {
/// Provided if we are relaying or receiving a payment within a blinded path, to decrypt the onion
/// routing packet and the recipient-provided encrypted payload within.
pub blinding_point: Option<PublicKey>,
+ /// Set to `Some` if the sender wants the receiver of this message to hold onto this HTLC until
+ /// receipt of a [`ReleaseHeldHtlc`] onion message from the payment recipient.
+ ///
+ /// [`ReleaseHeldHtlc`]: crate::onion_message::async_payments::ReleaseHeldHtlc
+ pub hold_htlc: Option<()>,
}
/// An onion message to be sent to or received from a peer.
@@ -3350,7 +3355,10 @@ impl_writeable_msg!(UpdateAddHTLC, {
onion_routing_packet,
}, {
(0, blinding_point, option),
- (65537, skimmed_fee_msat, option)
+ (65537, skimmed_fee_msat, option),
+ // TODO: currently we may fail to read the `ChannelManager` if we write a new even TLV in this message
+ // and then downgrade. Once this is fixed, update the type here to match BOLTs PR 989.
+ (75537, hold_htlc, option),
});
impl LengthReadable for OnionMessage {
@@ -5847,6 +5855,7 @@ mod tests {
onion_routing_packet,
skimmed_fee_msat: None,
blinding_point: None,
+ hold_htlc: None,
};
let encoded_value = update_add_htlc.encode();
let target_value = <Vec<u8>>::from_hex("020202020202020202020202020202020202020202020202020202020202020200083a840000034d32144668701144760101010101010101010101010101010101010101010101010101010101010101000c89d4ff031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010202020202020202020202020202020202020202020202020202020202020202").unwrap();
diff --git a/lightning/src/ln/onion_payment.rs b/lightning/src/ln/onion_payment.rs
index 79952fa..acaa030 100644
--- a/lightning/src/ln/onion_payment.rs
+++ b/lightning/src/ln/onion_payment.rs
@@ -753,6 +753,7 @@ mod tests {
onion_routing_packet,
skimmed_fee_msat: None,
blinding_point: None,
+ hold_htlc: None,
}
}
diff --git a/lightning/src/ln/payment_tests.rs b/lightning/src/ln/payment_tests.rs
index 0af0463..3dab164 100644
--- a/lightning/src/ln/payment_tests.rs
+++ b/lightning/src/ln/payment_tests.rs
@@ -5017,6 +5017,7 @@ fn peel_payment_onion_custom_tlvs() {
skimmed_fee_msat: None,
onion_routing_packet,
blinding_point: None,
+ hold_htlc: None,
};
let peeled_onion = crate::ln::onion_payment::peel_payment_onion(
&update_add,
Why this scored 19/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.