Add RevokeAndACK::release_htlc_message_paths
What changed, and why it matters
This commit adds a new optional data field called release_htlc_message_paths to the RevokeAndACK message used in the Lightning protocol. The field is intended to support a future feature where an offline payment sender can receive a reply when a held payment is released. The commit only defines and serializes the field; it always sets it to an empty list and does not yet implement the actual feature. There is no immediate security vulnerability visible in the change itself.
No immediate action required. Treat as a protocol feature addition. When the feature is completed, review how blinded paths are validated, how htlc_id mapping is authenticated, and whether malicious or oversized release_htlc_message_paths could be used to deanonymize, DoS, or misroute onion message replies.
Security signals we found
New TLV field added to a protocol message (RevokeAndACK)
Serialization macro changed to support Vec TLV fields
Field is always initialized to empty vector in current code
Feature is described as not yet enabled in commit message
Evidence from the diff
The patch extends the RevokeAndACK struct with a Vec<(u64, BlindedMessagePath)> TLV field (type 75537) named release_htlc_message_paths. It updates impl_writeable_msg! serialization macros to handle Vec-typed TLV fields by passing the field directly rather than as_ref(), and initializes TLV fields via a helper macro. All production and test construction sites initialize the new field to Vec::new(). The field is not populated with real data, so the async-payments reply-path behavior it is meant to enable is not yet active.
Changed components
lightning/src/ln/msgs.rs (RevokeAndACK message definition and serialization)lightning/src/util/ser_macros.rs (impl_writeable_msg! macro)lightning/src/ln/channel.rs (RevokeAndACK construction)lightning/src/ln/functional_tests.rslightning/src/ln/htlc_reserve_unit_tests.rsInspect captured patch +20 / −4
diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs
index 2a95e4f..17031cc 100644
--- a/lightning/src/ln/channel.rs
+++ b/lightning/src/ln/channel.rs
@@ -8998,6 +8998,7 @@ where
next_per_commitment_point: self.holder_commitment_point.next_point(),
#[cfg(taproot)]
next_local_nonce: None,
+ release_htlc_message_paths: Vec::new(),
});
}
}
diff --git a/lightning/src/ln/functional_tests.rs b/lightning/src/ln/functional_tests.rs
index d398d59..4d5355a 100644
--- a/lightning/src/ln/functional_tests.rs
+++ b/lightning/src/ln/functional_tests.rs
@@ -6548,6 +6548,7 @@ pub fn test_counterparty_raa_skip_no_crash() {
next_per_commitment_point,
#[cfg(taproot)]
next_local_nonce: None,
+ release_htlc_message_paths: Vec::new(),
};
nodes[1].node.handle_revoke_and_ack(node_a_id, &raa);
assert_eq!(
diff --git a/lightning/src/ln/htlc_reserve_unit_tests.rs b/lightning/src/ln/htlc_reserve_unit_tests.rs
index 7f411a6..dc5d07c 100644
--- a/lightning/src/ln/htlc_reserve_unit_tests.rs
+++ b/lightning/src/ln/htlc_reserve_unit_tests.rs
@@ -936,6 +936,7 @@ pub fn do_test_fee_spike_buffer(cfg: Option<UserConfig>, htlc_fails: bool) {
next_per_commitment_point: next_local_point,
#[cfg(taproot)]
next_local_nonce: None,
+ release_htlc_message_paths: Vec::new(),
};
nodes[1].node.handle_revoke_and_ack(node_a_id, &raa_msg);
expect_and_process_pending_htlcs(&nodes[1], false);
@@ -2381,6 +2382,7 @@ pub fn do_test_dust_limit_fee_accounting(can_afford: bool) {
next_per_commitment_point: next_local_point,
#[cfg(taproot)]
next_local_nonce: None,
+ release_htlc_message_paths: Vec::new(),
};
nodes[1].node.handle_revoke_and_ack(node_a_id, &raa_msg);
diff --git a/lightning/src/ln/msgs.rs b/lightning/src/ln/msgs.rs
index ef7e230..b3b2134 100644
--- a/lightning/src/ln/msgs.rs
+++ b/lightning/src/ln/msgs.rs
@@ -31,6 +31,7 @@ use bitcoin::secp256k1::ecdsa::Signature;
use bitcoin::secp256k1::PublicKey;
use bitcoin::{secp256k1, Transaction, Witness};
+use crate::blinded_path::message::BlindedMessagePath;
use crate::blinded_path::payment::{
BlindedPaymentTlvs, ForwardTlvs, ReceiveTlvs, UnauthenticatedReceiveTlvs,
};
@@ -888,6 +889,13 @@ pub struct RevokeAndACK {
#[cfg(taproot)]
/// Musig nonce the recipient should use in their next commitment signature message
pub next_local_nonce: Option<musig2::types::PublicNonce>,
+ /// A list of `(htlc_id, blinded_path)`. The receiver of this message will use the blinded paths
+ /// as reply paths to [`HeldHtlcAvailable`] onion messages that they send to the often-offline
+ /// receiver of this HTLC. The `htlc_id` is used by the receiver of this message to identify which
+ /// held HTLC a given blinded path corresponds to.
+ ///
+ /// [`HeldHtlcAvailable`]: crate::onion_message::async_payments::HeldHtlcAvailable
+ pub release_htlc_message_paths: Vec<(u64, BlindedMessagePath)>,
}
/// An [`update_fee`] message to be sent to or received from a peer
@@ -3260,7 +3268,9 @@ impl_writeable_msg!(RevokeAndACK, {
channel_id,
per_commitment_secret,
next_per_commitment_point
-}, {});
+}, {
+ (75537, release_htlc_message_paths, optional_vec)
+});
#[cfg(taproot)]
impl_writeable_msg!(RevokeAndACK, {
@@ -3268,7 +3278,8 @@ impl_writeable_msg!(RevokeAndACK, {
per_commitment_secret,
next_per_commitment_point
}, {
- (4, next_local_nonce, option)
+ (4, next_local_nonce, option),
+ (75537, release_htlc_message_paths, optional_vec)
});
impl_writeable_msg!(Shutdown, {
@@ -5976,6 +5987,7 @@ mod tests {
next_per_commitment_point: pubkey_1,
#[cfg(taproot)]
next_local_nonce: None,
+ release_htlc_message_paths: Vec::new(),
};
let encoded_value = raa.encode();
let target_value = <Vec<u8>>::from_hex("02020202020202020202020202020202020202020202020202020202020202020101010101010101010101010101010101010101010101010101010101010101031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f").unwrap();
diff --git a/lightning/src/util/ser_macros.rs b/lightning/src/util/ser_macros.rs
index ea7a3e8..647e7c7 100644
--- a/lightning/src/util/ser_macros.rs
+++ b/lightning/src/util/ser_macros.rs
@@ -700,7 +700,7 @@ macro_rules! impl_writeable_msg {
impl $crate::util::ser::Writeable for $st {
fn write<W: $crate::util::ser::Writer>(&self, w: &mut W) -> Result<(), $crate::io::Error> {
$( self.$field.write(w)?; )*
- $crate::encode_tlv_stream!(w, {$(($type, self.$tlvfield.as_ref(), $fieldty)),*});
+ $crate::encode_tlv_stream!(w, {$(($type, &self.$tlvfield, $fieldty)),*});
Ok(())
}
}
@@ -713,7 +713,7 @@ macro_rules! impl_writeable_msg {
$crate::decode_tlv_stream!(r, {$(($type, $tlvfield, $fieldty)),*});
Ok(Self {
$($field,)*
- $($tlvfield),*
+ $($tlvfield: $crate::_init_tlv_based_struct_field!($tlvfield, $fieldty)),*
})
}
}
Why this scored 18/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.