Intercept onion messages for unknown SCID hops
What changed, and why it matters
This commit adds a new optional feature to LDK's onion message forwarding. Previously, if an onion message's next hop was identified by a short channel ID (SCID) that the node didn't know, the message was silently dropped. Now, if a user explicitly enables 'intercept_for_unknown_scids', the node can hold onto the message and emit an event, allowing external protocols (like LSPS-style services) to resolve the SCID out-of-band and forward the message later. The change also updates how these intercepted events are serialized for backwards compatibility with older LDK versions.
Review the new interception API and serialization behavior for correctness; ensure that user-facing documentation clearly warns that enabling intercept_for_unknown_scids produces events that cannot be downgraded to LDK 0.2. Consider whether persisted intercepted messages could be replayed or abused, and verify that re-injected messages are validated before forwarding.
Security signals we found
New interception path for onion messages with unresolvable SCID next hops
Event serialization change with backward-incompatible ShortChannelId variant
Behavior gated by explicit opt-in flag intercept_for_unknown_scids
Potential for intercepted messages to be persisted and re-injected by user code
Serialization downgrade test confirms LDK 0.2 cannot read ShortChannelId variant
Evidence from the diff
The patch extends OnionMessenger to optionally intercept blinded onion-message hops that use an unresolvable ShortChannelId. It introduces a new boolean flag intercept_for_unknown_scids in OnionMessenger construction, changes Event::OnionMessageIntercepted to carry a NextMessageHop enum (NodeId or ShortChannelId) instead of a bare PublicKey, and adds serialization logic so that NodeId variants remain compatible with LDK 0.2 while ShortChannelId variants are not backward-compatible. Tests cover the new interception path, resolved-but-offline SCID behavior, and upgrade/downgrade serialization.
Changed components
lightning/src/onion_message/messenger.rslightning/src/events/mod.rslightning/src/blinded_path/message.rslightning/src/onion_message/functional_tests.rslightning-tests/src/upgrade_downgrade_tests.rslightning/src/ln/functional_test_utils.rsInspect captured patch +364 / −33
diff --git a/lightning-tests/src/upgrade_downgrade_tests.rs b/lightning-tests/src/upgrade_downgrade_tests.rs
index 7cc5922..75413ef 100644
--- a/lightning-tests/src/upgrade_downgrade_tests.rs
+++ b/lightning-tests/src/upgrade_downgrade_tests.rs
@@ -17,7 +17,10 @@ use lightning_0_2::ln::channelmanager::PaymentId as PaymentId_0_2;
use lightning_0_2::ln::channelmanager::RecipientOnionFields as RecipientOnionFields_0_2;
use lightning_0_2::ln::functional_test_utils as lightning_0_2_utils;
use lightning_0_2::ln::msgs::ChannelMessageHandler as _;
+use lightning_0_2::ln::msgs::OnionMessage as OnionMessage_0_2;
+use lightning_0_2::onion_message::packet::Packet as Packet_0_2;
use lightning_0_2::routing::router as router_0_2;
+use lightning_0_2::util::ser::MaybeReadable as MaybeReadable_0_2;
use lightning_0_2::util::ser::Writeable as _;
use lightning_0_1::commitment_signed_dance as commitment_signed_dance_0_1;
@@ -45,23 +48,29 @@ use lightning_0_0_125::ln::msgs::ChannelMessageHandler as _;
use lightning_0_0_125::routing::router as router_0_0_125;
use lightning_0_0_125::util::ser::Writeable as _;
+use lightning::blinded_path::message::NextMessageHop;
use lightning::chain::channelmonitor::{ANTI_REORG_DELAY, HTLC_FAIL_BACK_BUFFER};
use lightning::events::{ClosureReason, Event, HTLCHandlingFailureType};
use lightning::ln::functional_test_utils::*;
+use lightning::ln::msgs;
use lightning::ln::msgs::BaseMessageHandler as _;
use lightning::ln::msgs::ChannelMessageHandler as _;
use lightning::ln::msgs::MessageSendEvent;
use lightning::ln::splicing_tests::*;
use lightning::ln::types::ChannelId;
+use lightning::onion_message::packet::Packet;
use lightning::sign::OutputSpender;
+use lightning::util::ser::{MaybeReadable, Writeable};
use lightning::util::wallet_utils::WalletSourceSync;
use lightning_types::payment::{PaymentHash, PaymentPreimage, PaymentSecret};
use bitcoin::script::Builder;
-use bitcoin::secp256k1::Secp256k1;
+use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey};
use bitcoin::{opcodes, Amount, TxOut};
+use lightning::io::Cursor;
+
use std::sync::Arc;
#[test]
@@ -700,3 +709,107 @@ fn do_upgrade_mid_htlc_forward(test: MidHtlcForwardCase) {
expect_payment_claimable!(nodes[2], pay_hash, pay_secret, 1_000_000);
claim_payment(&nodes[0], &[&nodes[1], &nodes[2]], pay_preimage);
}
+
+/// Constructs a dummy `OnionMessage` (current version) for use in serialization tests.
+fn dummy_onion_message() -> msgs::OnionMessage {
+ let pubkey =
+ PublicKey::from_secret_key(&Secp256k1::new(), &SecretKey::from_slice(&[42; 32]).unwrap());
+ msgs::OnionMessage {
+ blinding_point: pubkey,
+ onion_routing_packet: Packet {
+ version: 0,
+ public_key: pubkey,
+ hop_data: vec![1; 64],
+ hmac: [2; 32],
+ },
+ }
+}
+
+/// Constructs a dummy `OnionMessage` (0.2 version) for use in serialization tests.
+fn dummy_onion_message_0_2() -> OnionMessage_0_2 {
+ let pubkey = bitcoin::secp256k1::PublicKey::from_secret_key(
+ &Secp256k1::new(),
+ &SecretKey::from_slice(&[42; 32]).unwrap(),
+ );
+ OnionMessage_0_2 {
+ blinding_point: pubkey,
+ onion_routing_packet: Packet_0_2 {
+ version: 0,
+ public_key: pubkey,
+ hop_data: vec![1; 64],
+ hmac: [2; 32],
+ },
+ }
+}
+
+#[test]
+fn test_onion_message_intercepted_upgrade_from_0_2() {
+ // Ensure that an `Event::OnionMessageIntercepted` serialized by LDK 0.2 (which uses
+ // `peer_node_id: PublicKey` in TLV field 0) can be deserialized by the current version,
+ // producing `NextMessageHop::NodeId`.
+ let pubkey =
+ PublicKey::from_secret_key(&Secp256k1::new(), &SecretKey::from_slice(&[42; 32]).unwrap());
+
+ let event_0_2 = Event_0_2::OnionMessageIntercepted {
+ peer_node_id: pubkey,
+ message: dummy_onion_message_0_2(),
+ };
+
+ let serialized = lightning_0_2::util::ser::Writeable::encode(&event_0_2);
+
+ let mut reader = Cursor::new(&serialized);
+ let deserialized = <Event as MaybeReadable>::read(&mut reader).unwrap().unwrap();
+
+ match deserialized {
+ Event::OnionMessageIntercepted { next_hop, message } => {
+ assert_eq!(next_hop, NextMessageHop::NodeId(pubkey));
+ assert_eq!(message, dummy_onion_message());
+ },
+ _ => panic!("Expected OnionMessageIntercepted event"),
+ }
+}
+
+#[test]
+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).
+ let pubkey =
+ PublicKey::from_secret_key(&Secp256k1::new(), &SecretKey::from_slice(&[42; 32]).unwrap());
+
+ let event = Event::OnionMessageIntercepted {
+ next_hop: NextMessageHop::NodeId(pubkey),
+ message: dummy_onion_message(),
+ };
+
+ let serialized = event.encode();
+
+ let mut reader = Cursor::new(&serialized);
+ let deserialized = <Event_0_2 as MaybeReadable_0_2>::read(&mut reader).unwrap().unwrap();
+
+ match deserialized {
+ Event_0_2::OnionMessageIntercepted { peer_node_id, message } => {
+ assert_eq!(peer_node_id, pubkey);
+ assert_eq!(message, dummy_onion_message_0_2());
+ },
+ _ => panic!("Expected OnionMessageIntercepted event"),
+ }
+}
+
+#[test]
+fn test_onion_message_intercepted_scid_downgrade_to_0_2() {
+ // Ensure that an `Event::OnionMessageIntercepted` with a `ShortChannelId` next hop
+ // 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 {
+ next_hop: NextMessageHop::ShortChannelId(42),
+ message: dummy_onion_message(),
+ };
+
+ let serialized = event.encode();
+
+ // LDK 0.2 will try to read field 0 as required. Since it's absent, the read will fail.
+ let mut reader = Cursor::new(&serialized);
+ let result = <Event_0_2 as MaybeReadable_0_2>::read(&mut reader);
+ assert!(result.is_err(), "LDK 0.2 should fail to decode a ShortChannelId variant");
+}
diff --git a/lightning/src/blinded_path/message.rs b/lightning/src/blinded_path/message.rs
index 417c663..65a3a45 100644
--- a/lightning/src/blinded_path/message.rs
+++ b/lightning/src/blinded_path/message.rs
@@ -275,6 +275,11 @@ pub enum NextMessageHop {
ShortChannelId(u64),
}
+impl_ser_tlv_based_enum!(NextMessageHop,
+ {0, NodeId} => (),
+ {2, ShortChannelId} => (),
+);
+
/// An intermediate node, and possibly a short channel id leading to the next node.
///
/// Note:
diff --git a/lightning/src/events/mod.rs b/lightning/src/events/mod.rs
index 4853c83..d6298a7 100644
--- a/lightning/src/events/mod.rs
+++ b/lightning/src/events/mod.rs
@@ -18,7 +18,7 @@ pub mod bump_transaction;
pub use bump_transaction::BumpTransactionEvent;
-use crate::blinded_path::message::{BlindedMessagePath, OffersContext};
+use crate::blinded_path::message::{BlindedMessagePath, NextMessageHop, OffersContext};
use crate::blinded_path::payment::{
Bolt12OfferContext, Bolt12RefundContext, PaymentContext, PaymentContextRef,
};
@@ -1836,9 +1836,13 @@ pub enum Event {
/// [`ChannelHandshakeConfig::negotiate_anchor_zero_fee_commitments`]: crate::util::config::ChannelHandshakeConfig::negotiate_anchor_zero_fee_commitments
BumpTransaction(BumpTransactionEvent),
/// We received an onion message that is intended to be forwarded to a peer
- /// that is currently offline. This event will only be generated if the
- /// `OnionMessenger` was initialized with
- /// [`OnionMessenger::new_with_offline_peer_interception`], see its docs.
+ /// that is currently offline *or* that is intended to be forwarded along a channel with an
+ /// SCID unknown to us.
+ ///
+ /// This event will only be generated if the `OnionMessenger` was initialized with
+ /// [`OnionMessenger::new_with_offline_peer_interception`], see its docs. The
+ /// [`NextMessageHop::ShortChannelId`] variant is only generated if `intercept_for_unknown_scids`
+ /// was set when constructing the `OnionMessenger`.
///
/// The offline peer should be awoken if possible on receipt of this event, such as via the LSPS5
/// protocol.
@@ -1852,9 +1856,10 @@ 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 offline peer.
- peer_node_id: PublicKey,
- /// The onion message intended to be forwarded to `peer_node_id`.
+ /// 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
+ /// channel once established.
message: msgs::OnionMessage,
},
/// Indicates that an onion message supporting peer has come online and any messages previously
@@ -2436,12 +2441,25 @@ impl Writeable for Event {
35u8.write(writer)?;
// Never write ConnectionNeeded events as buffered onion messages aren't serialized.
},
- &Event::OnionMessageIntercepted { ref peer_node_id, ref message } => {
+ &Event::OnionMessageIntercepted { ref next_hop, ref message } => {
37u8.write(writer)?;
- write_tlv_fields!(writer, {
- (0, peer_node_id, required),
- (2, message, required),
- });
+ 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),
+ });
+ },
+ }
},
&Event::OnionMessagePeerConnected { ref peer_node_id } => {
39u8.write(writer)?;
@@ -3069,11 +3087,16 @@ impl MaybeReadable for Event {
37u8 => {
let mut f = || {
_init_and_read_len_prefixed_tlv_fields!(reader, {
- (0, peer_node_id, required),
+ (0, peer_node_id, option),
+ (1, next_hop, option),
(2, message, required),
});
+
+ let next_hop = next_hop
+ .or(peer_node_id.map(NextMessageHop::NodeId))
+ .ok_or(msgs::DecodeError::InvalidValue)?;
Ok(Some(Event::OnionMessageIntercepted {
- peer_node_id: peer_node_id.0.unwrap(),
+ next_hop,
message: message.0.unwrap(),
}))
};
diff --git a/lightning/src/ln/functional_test_utils.rs b/lightning/src/ln/functional_test_utils.rs
index 20fcbef..82c1c61 100644
--- a/lightning/src/ln/functional_test_utils.rs
+++ b/lightning/src/ln/functional_test_utils.rs
@@ -4799,6 +4799,7 @@ pub fn create_network<'a, 'b: 'a, 'c: 'b>(
&chan_mgrs[i],
IgnoringMessageHandler {},
IgnoringMessageHandler {},
+ true,
);
let gossip_sync = P2PGossipSync::new(cfgs[i].network_graph.as_ref(), None, cfgs[i].logger);
let wallet_source = Arc::new(test_utils::TestWalletSource::new(
diff --git a/lightning/src/onion_message/functional_tests.rs b/lightning/src/onion_message/functional_tests.rs
index 75e2aaf..3692858 100644
--- a/lightning/src/onion_message/functional_tests.rs
+++ b/lightning/src/onion_message/functional_tests.rs
@@ -24,10 +24,10 @@ use super::offers::{OffersMessage, OffersMessageHandler};
use super::packet::{OnionMessageContents, Packet};
use crate::blinded_path::message::{
AsyncPaymentsContext, BlindedMessagePath, DNSResolverContext, MessageContext,
- MessageForwardNode, OffersContext, MESSAGE_PADDING_ROUND_OFF,
+ MessageForwardNode, NextMessageHop, OffersContext, MESSAGE_PADDING_ROUND_OFF,
};
use crate::blinded_path::utils::is_padded;
-use crate::blinded_path::EmptyNodeIdLookUp;
+use crate::blinded_path::NodeIdLookUp;
use crate::events::{Event, EventsProvider};
use crate::ln::msgs::{self, BaseMessageHandler, DecodeError, OnionMessageHandler};
use crate::routing::gossip::{NetworkGraph, P2PGossipSync};
@@ -60,17 +60,40 @@ struct MessengerNode {
Arc<TestKeysInterface>,
Arc<TestNodeSigner>,
Arc<TestLogger>,
- Arc<EmptyNodeIdLookUp>,
+ Arc<TestNodeIdLookUp>,
Arc<MessageRouter>,
Arc<TestOffersMessageHandler>,
Arc<TestAsyncPaymentsMessageHandler>,
Arc<TestDNSResolverMessageHandler>,
Arc<TestCustomMessageHandler>,
>,
+ node_id_lookup: Arc<TestNodeIdLookUp>,
custom_message_handler: Arc<TestCustomMessageHandler>,
gossip_sync: Arc<P2PGossipSync<Arc<NetGraph>, Arc<TestChainSource>, Arc<TestLogger>>>,
}
+/// A [`NodeIdLookUp`] that resolves SCIDs to node ids from an insertable map (empty by default,
+/// so it behaves like an empty lookup unless a mapping is added).
+struct TestNodeIdLookUp {
+ scid_to_node_id: Mutex<HashMap<u64, PublicKey>>,
+}
+
+impl TestNodeIdLookUp {
+ fn new() -> Self {
+ Self { scid_to_node_id: Mutex::new(new_hash_map()) }
+ }
+
+ fn add_mapping(&self, scid: u64, node_id: PublicKey) {
+ self.scid_to_node_id.lock().unwrap().insert(scid, node_id);
+ }
+}
+
+impl NodeIdLookUp for TestNodeIdLookUp {
+ fn next_node_id(&self, short_channel_id: u64) -> Option<PublicKey> {
+ self.scid_to_node_id.lock().unwrap().get(&short_channel_id).copied()
+ }
+}
+
impl Drop for MessengerNode {
fn drop(&mut self) {
if std::thread::panicking() {
@@ -275,10 +298,15 @@ fn create_nodes(num_messengers: u8) -> Vec<MessengerNode> {
struct MessengerCfg {
secret_override: Option<SecretKey>,
intercept_offline_peer_oms: bool,
+ intercept_unknown_scid_oms: bool,
}
impl MessengerCfg {
fn new() -> Self {
- Self { secret_override: None, intercept_offline_peer_oms: false }
+ Self {
+ secret_override: None,
+ intercept_offline_peer_oms: false,
+ intercept_unknown_scid_oms: false,
+ }
}
fn with_node_secret(mut self, secret: SecretKey) -> Self {
self.secret_override = Some(secret);
@@ -288,6 +316,10 @@ impl MessengerCfg {
self.intercept_offline_peer_oms = true;
self
}
+ fn with_unknown_scid_interception(mut self) -> Self {
+ self.intercept_unknown_scid_oms = true;
+ self
+ }
}
fn create_nodes_using_cfgs(cfgs: Vec<MessengerCfg>) -> Vec<MessengerNode> {
@@ -304,31 +336,32 @@ fn create_nodes_using_cfgs(cfgs: Vec<MessengerCfg>) -> Vec<MessengerNode> {
let entropy_source = Arc::new(TestKeysInterface::new(&seed, Network::Testnet));
let node_signer = Arc::new(TestNodeSigner::new(secret_key));
- let node_id_lookup = Arc::new(EmptyNodeIdLookUp {});
+ let node_id_lookup = Arc::new(TestNodeIdLookUp::new());
let message_router =
DefaultMessageRouter::new(Arc::clone(&network_graph), Arc::clone(&entropy_source));
let offers_message_handler = Arc::new(TestOffersMessageHandler {});
let async_payments_message_handler = Arc::new(TestAsyncPaymentsMessageHandler {});
let dns_resolver_message_handler = Arc::new(TestDNSResolverMessageHandler {});
let custom_message_handler = Arc::new(TestCustomMessageHandler::new());
- let messenger = if cfg.intercept_offline_peer_oms {
+ let messenger = if cfg.intercept_offline_peer_oms || cfg.intercept_unknown_scid_oms {
OnionMessenger::new_with_offline_peer_interception(
Arc::clone(&entropy_source),
Arc::clone(&node_signer),
logger,
- node_id_lookup,
+ Arc::clone(&node_id_lookup),
Arc::new(message_router),
offers_message_handler,
async_payments_message_handler,
dns_resolver_message_handler,
Arc::clone(&custom_message_handler),
+ cfg.intercept_unknown_scid_oms,
)
} else {
OnionMessenger::new(
Arc::clone(&entropy_source),
Arc::clone(&node_signer),
logger,
- node_id_lookup,
+ Arc::clone(&node_id_lookup),
Arc::new(message_router),
offers_message_handler,
async_payments_message_handler,
@@ -341,6 +374,7 @@ fn create_nodes_using_cfgs(cfgs: Vec<MessengerCfg>) -> Vec<MessengerNode> {
node_id: node_signer.get_node_id(Recipient::Node).unwrap(),
entropy_source,
messenger,
+ node_id_lookup,
custom_message_handler,
gossip_sync: Arc::clone(&gossip_sync),
});
@@ -1144,9 +1178,13 @@ 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 { peer_node_id, message } => {
- assert_eq!(peer_node_id, final_node_vec[0].node_id);
- message
+ Event::OnionMessageIntercepted { next_hop, message } => {
+ if let NextMessageHop::NodeId(peer_node_id) = next_hop {
+ assert_eq!(peer_node_id, final_node_vec[0].node_id);
+ message
+ } else {
+ panic!();
+ }
},
_ => panic!(),
};
@@ -1173,6 +1211,129 @@ fn intercept_offline_peer_oms() {
pass_along_path(&vec![nodes.remove(1), final_node_vec.remove(0)]);
}
+#[test]
+fn intercept_unknown_scid_oms() {
+ // Ensure that if OnionMessenger is initialized with
+ // new_with_offline_peer_interception and `intercept_for_unknown_scids` set, we will
+ // intercept OMs that use an unknown SCID as the next hop, generate the right events, and
+ // forward OMs when they are re-injected by the user.
+ let node_cfgs = vec![
+ MessengerCfg::new(),
+ MessengerCfg::new().with_unknown_scid_interception(),
+ MessengerCfg::new(),
+ ];
+ let mut nodes = create_nodes_using_cfgs(node_cfgs);
+
+ let peer_conn_evs = release_events(&nodes[1]);
+ assert_eq!(peer_conn_evs.len(), 2);
+ for (i, ev) in peer_conn_evs.iter().enumerate() {
+ match ev {
+ Event::OnionMessagePeerConnected { peer_node_id } => {
+ let node_idx = if i == 0 { 0 } else { 2 };
+ assert_eq!(peer_node_id, &nodes[node_idx].node_id);
+ },
+ _ => panic!(),
+ }
+ }
+
+ // Use a SCID-based intermediate hop to trigger the unknown SCID interception path. Since no
+ // mapping was added to the `TestNodeIdLookUp`, the SCID cannot be resolved, so the
+ // OnionMessenger will generate an `OnionMessageIntercepted` event with a `ShortChannelId`
+ // next hop.
+ let scid = 42;
+ let message = TestCustomMessage::Pong;
+ let intermediate_nodes =
+ [MessageForwardNode { node_id: nodes[1].node_id, short_channel_id: Some(scid) }];
+ let blinded_path = BlindedMessagePath::new(
+ &intermediate_nodes,
+ nodes[2].node_id,
+ nodes[2].messenger.node_signer.get_receive_auth_key(),
+ MessageContext::Custom(Vec::new()),
+ false,
+ &*nodes[2].entropy_source,
+ &Secp256k1::new(),
+ );
+ let destination = Destination::BlindedPath(blinded_path);
+ let instructions = MessageSendInstructions::WithoutReplyPath { destination };
+
+ nodes[0].messenger.send_onion_message(message, instructions).unwrap();
+ let mut final_node_vec = nodes.split_off(2);
+ pass_along_path(&nodes);
+
+ // We expect an `OnionMessageIntercepted` event with a `ShortChannelId` next hop since the
+ // SCID is not resolvable (no mapping was added to the `TestNodeIdLookUp`).
+ let mut events = release_events(&nodes[1]);
+ assert_eq!(events.len(), 1);
+ let onion_message = match events.remove(0) {
+ Event::OnionMessageIntercepted { next_hop, message } => {
+ if let NextMessageHop::ShortChannelId(intercepted_scid) = next_hop {
+ assert_eq!(intercepted_scid, scid);
+ message
+ } else {
+ panic!("Expected ShortChannelId next hop, got NodeId");
+ }
+ },
+ _ => panic!(),
+ };
+
+ // The user resolves the SCID externally and forwards the intercepted message to the
+ // correct peer.
+ nodes[1].messenger.forward_onion_message(onion_message, &final_node_vec[0].node_id).unwrap();
+ final_node_vec[0].custom_message_handler.expect_message(TestCustomMessage::Pong);
+ pass_along_path(&vec![nodes.remove(1), final_node_vec.remove(0)]);
+}
+
+#[test]
+fn intercept_resolved_scid_offline_peer_oms() {
+ // Ensure that when a forwarded OM's next hop is a SCID that resolves to a known but offline
+ // peer, the offline-peer interception path reports the resolved node id rather than the SCID,
+ // even though `intercept_for_unknown_scids` is disabled.
+ let node_cfgs = vec![
+ MessengerCfg::new(),
+ MessengerCfg::new().with_offline_peer_interception(),
+ MessengerCfg::new(),
+ ];
+ let mut nodes = create_nodes_using_cfgs(node_cfgs);
+
+ // Clear the initial `OnionMessagePeerConnected` events.
+ let _ = release_events(&nodes[1]);
+
+ // Resolve the SCID to nodes[2] and disconnect it so it appears as a known-but-offline peer.
+ let scid = 42;
+ nodes[1].node_id_lookup.add_mapping(scid, nodes[2].node_id);
+ disconnect_peers(&nodes[1], &nodes[2]);
+
+ let message = TestCustomMessage::Pong;
+ let intermediate_nodes =
+ [MessageForwardNode { node_id: nodes[1].node_id, short_channel_id: Some(scid) }];
+ let blinded_path = BlindedMessagePath::new(
+ &intermediate_nodes,
+ nodes[2].node_id,
+ nodes[2].messenger.node_signer.get_receive_auth_key(),
+ MessageContext::Custom(Vec::new()),
+ false,
+ &*nodes[2].entropy_source,
+ &Secp256k1::new(),
+ );
+ let destination = Destination::BlindedPath(blinded_path);
+ let instructions = MessageSendInstructions::WithoutReplyPath { destination };
+
+ nodes[0].messenger.send_onion_message(message, instructions).unwrap();
+ let final_node_vec = nodes.split_off(2);
+ pass_along_path(&nodes);
+
+ // The next hop resolved to a known (but offline) peer, so the event must carry its node id
+ // rather than the SCID variant (which `intercept_for_unknown_scids` would have produced).
+ let mut events = release_events(&nodes[1]);
+ assert_eq!(events.len(), 1);
+ match events.remove(0) {
+ Event::OnionMessageIntercepted { next_hop, .. } => {
+ assert_eq!(next_hop, NextMessageHop::NodeId(final_node_vec[0].node_id));
+ },
+ _ => panic!(),
+ }
+}
+
#[test]
fn spec_test_vector() {
let node_cfgs = [
diff --git a/lightning/src/onion_message/messenger.rs b/lightning/src/onion_message/messenger.rs
index 913a046..2070ff9 100644
--- a/lightning/src/onion_message/messenger.rs
+++ b/lightning/src/onion_message/messenger.rs
@@ -273,6 +273,7 @@ pub struct OnionMessenger<
dns_resolver_handler: DRH,
custom_handler: CMH,
intercept_messages_for_offline_peers: bool,
+ intercept_for_unknown_scids: bool,
pending_intercepted_msgs_events: Mutex<Vec<Event>>,
pending_peer_connected_events: Mutex<Vec<Event>>,
pending_events_processor: AtomicBool,
@@ -1393,6 +1394,7 @@ impl<
dns_resolver,
custom_handler,
false,
+ false,
)
}
@@ -1400,11 +1402,18 @@ impl<
/// intended to be forwarded to offline peers, we will intercept them for
/// later forwarding.
///
+ /// If `intercept_for_unknown_scids` is set, we will additionally intercept onion messages whose
+ /// next hop is a [`NextMessageHop::ShortChannelId`] that cannot be resolved to a connected
+ /// peer, generating an [`Event::OnionMessageIntercepted`] with a
+ /// [`NextMessageHop::ShortChannelId`] next hop. This variant of the event was introduced in
+ /// LDK 0.3, so users who persist [`Event::OnionMessageIntercepted`] events and may need to
+ /// downgrade to LDK 0.2 must leave this disabled.
+ ///
/// Interception flow:
- /// 1. If an onion message for an offline peer is received, `OnionMessenger` will
- /// generate an [`Event::OnionMessageIntercepted`]. Event handlers can
- /// then choose to persist this onion message for later forwarding, or drop
- /// it.
+ /// 1. If an onion message for an offline peer or (if `intercept_for_unknown_scids` is set) an
+ /// unknown SCID is received, `OnionMessenger` will generate an
+ /// [`Event::OnionMessageIntercepted`]. Event handlers can then choose to persist this
+ /// onion message for later forwarding, or drop it.
/// 2. When the offline peer later comes back online, `OnionMessenger` will
/// generate an [`Event::OnionMessagePeerConnected`]. Event handlers will
/// then fetch all previously intercepted onion messages for this peer.
@@ -1420,6 +1429,7 @@ impl<
pub fn new_with_offline_peer_interception(
entropy_source: ES, node_signer: NS, logger: L, node_id_lookup: NL, message_router: MR,
offers_handler: OMH, async_payments_handler: APH, dns_resolver: DRH, custom_handler: CMH,
+ intercept_for_unknown_scids: bool,
) -> Self {
Self::new_inner(
entropy_source,
@@ -1432,13 +1442,14 @@ impl<
dns_resolver,
custom_handler,
true,
+ intercept_for_unknown_scids,
)
}
fn new_inner(
entropy_source: ES, node_signer: NS, logger: L, node_id_lookup: NL, message_router: MR,
offers_handler: OMH, async_payments_handler: APH, dns_resolver: DRH, custom_handler: CMH,
- intercept_messages_for_offline_peers: bool,
+ intercept_messages_for_offline_peers: bool, intercept_for_unknown_scids: bool,
) -> Self {
let mut secp_ctx = Secp256k1::new();
secp_ctx.seeded_randomize(&entropy_source.get_secure_random_bytes());
@@ -1455,6 +1466,7 @@ impl<
dns_resolver_handler: dns_resolver,
custom_handler,
intercept_messages_for_offline_peers,
+ intercept_for_unknown_scids,
pending_intercepted_msgs_events: Mutex::new(Vec::new()),
pending_peer_connected_events: Mutex::new(Vec::new()),
pending_events_processor: AtomicBool::new(false),
@@ -1666,7 +1678,20 @@ impl<
NextMessageHop::ShortChannelId(scid) => match self.node_id_lookup.next_node_id(scid) {
Some(pubkey) => pubkey,
None => {
- log_trace!(self.logger, "Dropping forwarded onion messager: unable to resolve next hop using SCID {} {}", scid, log_suffix);
+ if self.intercept_for_unknown_scids {
+ log_trace!(
+ self.logger,
+ "Generating OnionMessageIntercepted event for SCID {} {}",
+ scid,
+ log_suffix
+ );
+ self.enqueue_intercepted_event(Event::OnionMessageIntercepted {
+ next_hop,
+ message: onion_message,
+ });
+ return Ok(());
+ }
+ log_trace!(self.logger, "Dropping forwarded onion message: unable to resolve next hop using SCID {} {}", scid, log_suffix);
return Err(SendError::GetNodeIdFailed);
},
},
@@ -1709,7 +1734,10 @@ impl<
log_suffix
);
self.enqueue_intercepted_event(Event::OnionMessageIntercepted {
- peer_node_id: next_node_id,
+ // 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.
+ next_hop: NextMessageHop::NodeId(next_node_id),
message: onion_message,
});
Ok(())
Why this scored 37/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.