Report the sending peer in Event::OnionMessageIntercepted
What changed, and why it matters
This commit adds a new prev_hop field to an existing event type so that applications using the Lightning Dev Kit can see which peer sent an intercepted onion message. It is a feature enhancement that improves policy control; it does not by itself fix a vulnerability or introduce a clear security bug.
No security action required; treat as a normal API/feature update. Review downstream consumers of Event::OnionMessageIntercepted for compilation impact due to the new prev_hop field.
Security signals we found
New optional field added to an existing public event variant
Serialization/deserialization updated with TLV backwards-compatibility handling
No change to cryptographic validation, access control, or resource limits
No mention of vulnerability, CVE, or security bug in commit message or diff
Evidence from the diff
The change extends Event::OnionMessageIntercepted with an optional prev_hop PublicKey, updates serialization (TLV field 3) with backwards-compatible defaults for older versions, and threads the sender’s node id through OnionMessenger’s forwarding path. It is purely informational and does not alter message handling logic.
Changed components
lightning/src/events/mod.rslightning/src/onion_message/messenger.rslightning/src/onion_message/functional_tests.rslightning-tests/src/upgrade_downgrade_tests.rsInspect captured patch +44 / −23
diff --git a/lightning-tests/src/upgrade_downgrade_tests.rs b/lightning-tests/src/upgrade_downgrade_tests.rs
index 75413ef..136ae91 100644
--- a/lightning-tests/src/upgrade_downgrade_tests.rs
+++ b/lightning-tests/src/upgrade_downgrade_tests.rs
@@ -761,7 +761,9 @@ fn test_onion_message_intercepted_upgrade_from_0_2() {
let deserialized = <Event as MaybeReadable>::read(&mut reader).unwrap().unwrap();
match deserialized {
- Event::OnionMessageIntercepted { next_hop, message } => {
+ Event::OnionMessageIntercepted { prev_hop, next_hop, message } => {
+ // LDK 0.2 did not write a `prev_hop`, so it must default to `None`.
+ assert_eq!(prev_hop, None);
assert_eq!(next_hop, NextMessageHop::NodeId(pubkey));
assert_eq!(message, dummy_onion_message());
},
@@ -773,11 +775,14 @@ fn test_onion_message_intercepted_upgrade_from_0_2() {
fn test_onion_message_intercepted_node_id_downgrade_to_0_2() {
// Ensure that an `Event::OnionMessageIntercepted` with a `NodeId` next hop serialized by
// the current version can be deserialized by LDK 0.2 (which expects `peer_node_id` in TLV
- // field 0).
+ // field 0 and ignores the newer `prev_hop` in TLV field 3).
let pubkey =
PublicKey::from_secret_key(&Secp256k1::new(), &SecretKey::from_slice(&[42; 32]).unwrap());
+ let prev_hop =
+ PublicKey::from_secret_key(&Secp256k1::new(), &SecretKey::from_slice(&[43; 32]).unwrap());
let event = Event::OnionMessageIntercepted {
+ prev_hop: Some(prev_hop),
next_hop: NextMessageHop::NodeId(pubkey),
message: dummy_onion_message(),
};
@@ -802,6 +807,7 @@ fn test_onion_message_intercepted_scid_downgrade_to_0_2() {
// serialized by the current version cannot be deserialized by LDK 0.2, since the
// `peer_node_id` field (0) is not written for SCID variants and LDK 0.2 requires it.
let event = Event::OnionMessageIntercepted {
+ prev_hop: None,
next_hop: NextMessageHop::ShortChannelId(42),
message: dummy_onion_message(),
};
diff --git a/lightning/src/events/mod.rs b/lightning/src/events/mod.rs
index d6298a7..ec0ad6c 100644
--- a/lightning/src/events/mod.rs
+++ b/lightning/src/events/mod.rs
@@ -1856,6 +1856,17 @@ pub enum Event {
///
/// [`OnionMessenger::new_with_offline_peer_interception`]: crate::onion_message::messenger::OnionMessenger::new_with_offline_peer_interception
OnionMessageIntercepted {
+ /// The node id of the peer that sent the message, if known.
+ ///
+ /// This is `None` when the message is sent with
+ /// [`MessageSendInstructions::ForwardedMessage`] (e.g., when calling
+ /// [`OffersMessageFlow::enqueue_invoice_request_to_forward`]) rather than forwarded
+ /// internally by the `OnionMessenger`, as well as for events serialized prior to LDK 0.3.
+ /// Otherwise it is the node we received the message from.
+ ///
+ /// [`MessageSendInstructions::ForwardedMessage`]: crate::onion_message::messenger::MessageSendInstructions::ForwardedMessage
+ /// [`OffersMessageFlow::enqueue_invoice_request_to_forward`]: crate::offers::flow::OffersMessageFlow::enqueue_invoice_request_to_forward
+ prev_hop: Option<PublicKey>,
/// The next hop (offline peer or unknown SCID).
next_hop: NextMessageHop,
/// The onion message intended to be forwarded to the offline peer or via the unknown
@@ -2441,25 +2452,20 @@ impl Writeable for Event {
35u8.write(writer)?;
// Never write ConnectionNeeded events as buffered onion messages aren't serialized.
},
- &Event::OnionMessageIntercepted { ref next_hop, ref message } => {
+ &Event::OnionMessageIntercepted { ref prev_hop, ref next_hop, ref message } => {
37u8.write(writer)?;
- match next_hop {
- NextMessageHop::NodeId(peer_node_id) => {
- // If we have the node_id, we keep writing it for backwards compatibility.
- write_tlv_fields!(writer, {
- (0, peer_node_id, required),
- (1, next_hop, required),
- (2, message, required),
- });
- },
- NextMessageHop::ShortChannelId(_) => {
- write_tlv_fields!(writer, {
- // 0 used to be peer_node_id in LDK v0.2 and prior.
- (1, next_hop, required),
- (2, message, required),
- });
- },
- }
+ // 0 used to be peer_node_id in LDK v0.2 and prior; we keep writing it when the next
+ // hop is a node id for backwards compatibility.
+ let legacy_peer_node_id = match next_hop {
+ NextMessageHop::NodeId(node_id) => Some(node_id),
+ NextMessageHop::ShortChannelId(_) => None,
+ };
+ write_tlv_fields!(writer, {
+ (0, legacy_peer_node_id, option),
+ (1, next_hop, required),
+ (2, message, required),
+ (3, prev_hop, option),
+ });
},
&Event::OnionMessagePeerConnected { ref peer_node_id } => {
39u8.write(writer)?;
@@ -3090,12 +3096,14 @@ impl MaybeReadable for Event {
(0, peer_node_id, option),
(1, next_hop, option),
(2, message, required),
+ (3, prev_hop, option),
});
let next_hop = next_hop
.or(peer_node_id.map(NextMessageHop::NodeId))
.ok_or(msgs::DecodeError::InvalidValue)?;
Ok(Some(Event::OnionMessageIntercepted {
+ prev_hop,
next_hop,
message: message.0.unwrap(),
}))
diff --git a/lightning/src/onion_message/functional_tests.rs b/lightning/src/onion_message/functional_tests.rs
index 3692858..4adc126 100644
--- a/lightning/src/onion_message/functional_tests.rs
+++ b/lightning/src/onion_message/functional_tests.rs
@@ -1178,7 +1178,8 @@ fn intercept_offline_peer_oms() {
let mut events = release_events(&nodes[1]);
assert_eq!(events.len(), 1);
let onion_message = match events.remove(0) {
- Event::OnionMessageIntercepted { next_hop, message } => {
+ Event::OnionMessageIntercepted { prev_hop, next_hop, message } => {
+ assert_eq!(prev_hop, Some(nodes[0].node_id));
if let NextMessageHop::NodeId(peer_node_id) = next_hop {
assert_eq!(peer_node_id, final_node_vec[0].node_id);
message
@@ -1265,7 +1266,8 @@ fn intercept_unknown_scid_oms() {
let mut events = release_events(&nodes[1]);
assert_eq!(events.len(), 1);
let onion_message = match events.remove(0) {
- Event::OnionMessageIntercepted { next_hop, message } => {
+ Event::OnionMessageIntercepted { prev_hop, next_hop, message } => {
+ assert_eq!(prev_hop, Some(nodes[0].node_id));
if let NextMessageHop::ShortChannelId(intercepted_scid) = next_hop {
assert_eq!(intercepted_scid, scid);
message
diff --git a/lightning/src/onion_message/messenger.rs b/lightning/src/onion_message/messenger.rs
index 2070ff9..04697d9 100644
--- a/lightning/src/onion_message/messenger.rs
+++ b/lightning/src/onion_message/messenger.rs
@@ -1556,6 +1556,7 @@ impl<
let result = if is_forward {
self.enqueue_forwarded_onion_message(
+ None,
NextMessageHop::NodeId(first_node_id),
onion_message,
log_suffix,
@@ -1671,7 +1672,8 @@ impl<
}
fn enqueue_forwarded_onion_message(
- &self, next_hop: NextMessageHop, onion_message: OnionMessage, log_suffix: fmt::Arguments,
+ &self, prev_hop: Option<PublicKey>, next_hop: NextMessageHop, onion_message: OnionMessage,
+ log_suffix: fmt::Arguments,
) -> Result<(), SendError> {
let next_node_id = match next_hop {
NextMessageHop::NodeId(pubkey) => pubkey,
@@ -1686,6 +1688,7 @@ impl<
log_suffix
);
self.enqueue_intercepted_event(Event::OnionMessageIntercepted {
+ prev_hop,
next_hop,
message: onion_message,
});
@@ -1734,6 +1737,7 @@ impl<
log_suffix
);
self.enqueue_intercepted_event(Event::OnionMessageIntercepted {
+ prev_hop,
// Report the resolved node id rather than `next_hop`, which may be a
// `ShortChannelId` that we resolved to a known-but-offline peer. The
// `ShortChannelId` variant is reserved for the unknown-SCID interception path.
@@ -2318,6 +2322,7 @@ impl<
},
Ok(PeeledOnion::Forward(next_hop, onion_message)) => {
let _ = self.enqueue_forwarded_onion_message(
+ Some(peer_node_id),
next_hop,
onion_message,
format_args!("when forwarding peeled onion message from {}", peer_node_id),
Why this scored 20/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.