Skip to content

Commit 1e94a08

Browse files
committed
Introduce Payment Dummy Hop parsing mechanism
1 parent fe33666 commit 1e94a08

4 files changed

Lines changed: 196 additions & 37 deletions

File tree

lightning/src/blinded_path/payment.rs

Lines changed: 28 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ use crate::util::ser::{
3434
Writeable, Writer,
3535
};
3636

37-
use core::mem;
3837
use core::ops::Deref;
3938

4039
#[allow(unused_imports)]
@@ -219,28 +218,31 @@ impl BlindedPaymentPath {
219218
NL::Target: NodeIdLookUp,
220219
T: secp256k1::Signing + secp256k1::Verification,
221220
{
222-
match self.decrypt_intro_payload::<NS>(node_signer) {
223-
Ok((
224-
BlindedPaymentTlvs::Forward(ForwardTlvs { short_channel_id, .. }),
225-
control_tlvs_ss,
226-
)) => {
227-
let next_node_id = match node_id_lookup.next_node_id(short_channel_id) {
228-
Some(node_id) => node_id,
229-
None => return Err(()),
230-
};
231-
let mut new_blinding_point = onion_utils::next_hop_pubkey(
232-
secp_ctx,
233-
self.inner_path.blinding_point,
234-
control_tlvs_ss.as_ref(),
235-
)
236-
.map_err(|_| ())?;
237-
mem::swap(&mut self.inner_path.blinding_point, &mut new_blinding_point);
238-
self.inner_path.introduction_node = IntroductionNode::NodeId(next_node_id);
239-
self.inner_path.blinded_hops.remove(0);
240-
Ok(())
241-
},
242-
_ => Err(()),
243-
}
221+
let (next_node_id, control_tlvs_ss) =
222+
match self.decrypt_intro_payload::<NS>(node_signer).map_err(|_| ())? {
223+
(BlindedPaymentTlvs::Forward(ForwardTlvs { short_channel_id, .. }), ss) => {
224+
let node_id = node_id_lookup.next_node_id(short_channel_id).ok_or(())?;
225+
(node_id, ss)
226+
},
227+
(BlindedPaymentTlvs::Dummy, ss) => {
228+
let node_id = node_signer.get_node_id(Recipient::Node)?;
229+
(node_id, ss)
230+
},
231+
_ => return Err(()),
232+
};
233+
234+
let new_blinding_point = onion_utils::next_hop_pubkey(
235+
secp_ctx,
236+
self.inner_path.blinding_point,
237+
control_tlvs_ss.as_ref(),
238+
)
239+
.map_err(|_| ())?;
240+
241+
self.inner_path.blinding_point = new_blinding_point;
242+
self.inner_path.introduction_node = IntroductionNode::NodeId(next_node_id);
243+
self.inner_path.blinded_hops.remove(0);
244+
245+
Ok(())
244246
}
245247

246248
pub(crate) fn decrypt_intro_payload<NS: Deref>(
@@ -262,9 +264,9 @@ impl BlindedPaymentPath {
262264
.map_err(|_| ())?;
263265

264266
match (&readable, used_aad) {
265-
(BlindedPaymentTlvs::Forward(_), false) | (BlindedPaymentTlvs::Receive(_), true) => {
266-
Ok((readable, control_tlvs_ss))
267-
},
267+
(BlindedPaymentTlvs::Forward(_), false)
268+
| (BlindedPaymentTlvs::Dummy, true)
269+
| (BlindedPaymentTlvs::Receive(_), true) => Ok((readable, control_tlvs_ss)),
268270
_ => Err(()),
269271
}
270272
}

lightning/src/ln/channelmanager.rs

Lines changed: 120 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ use crate::ln::channel_state::ChannelDetails;
6666
use crate::ln::funding::SpliceContribution;
6767
use crate::ln::inbound_payment;
6868
use crate::ln::interactivetxs::InteractiveTxMessageSend;
69-
use crate::ln::msgs;
69+
use crate::ln::msgs::{self, OnionPacket, UpdateAddHTLC};
7070
use crate::ln::msgs::{
7171
BaseMessageHandler, ChannelMessageHandler, CommitmentUpdate, DecodeError, LightningError,
7272
MessageSendEvent,
@@ -229,6 +229,23 @@ pub enum PendingHTLCRouting {
229229
/// [`ReleaseHeldHtlc`] onion message.
230230
hold_htlc: Option<()>,
231231
},
232+
/// A dummy HTLC hop which does not represent a real routing decision.
233+
///
234+
/// Dummy HTLCs are introduced to extend a blinded path without adding a real hop.
235+
/// When such an HTLC is received, the dummy layer is peeled, producing a new onion
236+
/// packet which must be processed again. This process repeats until a non-dummy
237+
/// routing decision is reached, which is guaranteed to be
238+
/// [`PendingHTLCRouting::Receive`].
239+
Dummy {
240+
/// The onion packet obtained after removing the dummy layer.
241+
///
242+
/// This packet must be re-processed as if it were freshly received.
243+
onion_packet: msgs::OnionPacket,
244+
/// Forwarding context for this HTLC within a blinded path.
245+
blinded: BlindedForward,
246+
/// The absolute CLTV of the inbound HTLC.
247+
incoming_cltv_expiry: Option<u32>,
248+
},
232249
/// An HTLC which should be forwarded on to another Trampoline node.
233250
TrampolineForward {
234251
/// The onion shared secret we build with the sender (or the preceding Trampoline node) used
@@ -352,6 +369,7 @@ impl PendingHTLCRouting {
352369
fn blinded_failure(&self) -> Option<BlindedFailure> {
353370
match self {
354371
Self::Forward { blinded: Some(BlindedForward { failure, .. }), .. } => Some(*failure),
372+
Self::Dummy { blinded: BlindedForward { failure, .. }, .. } => Some(*failure),
355373
Self::TrampolineForward { blinded: Some(BlindedForward { failure, .. }), .. } => {
356374
Some(*failure)
357375
},
@@ -368,6 +386,7 @@ impl PendingHTLCRouting {
368386
fn incoming_cltv_expiry(&self) -> Option<u32> {
369387
match self {
370388
Self::Forward { incoming_cltv_expiry, .. } => *incoming_cltv_expiry,
389+
Self::Dummy { incoming_cltv_expiry, .. } => *incoming_cltv_expiry,
371390
Self::TrampolineForward { incoming_cltv_expiry, .. } => Some(*incoming_cltv_expiry),
372391
Self::Receive { incoming_cltv_expiry, .. } => Some(*incoming_cltv_expiry),
373392
Self::ReceiveKeysend { incoming_cltv_expiry, .. } => Some(*incoming_cltv_expiry),
@@ -4974,6 +4993,11 @@ where
49744993
) -> Result<(), LocalHTLCFailureReason> {
49754994
let outgoing_scid = match next_packet_details.outgoing_connector {
49764995
HopConnector::ShortChannelId(scid) => scid,
4996+
HopConnector::Dummy => {
4997+
// Dummy hops are only used for path padding and must not reach HTLC processing.
4998+
debug_assert!(false, "Dummy hop reached HTLC handling.");
4999+
return Err(LocalHTLCFailureReason::InvalidOnionPayload);
5000+
}
49775001
HopConnector::Trampoline(_) => {
49785002
return Err(LocalHTLCFailureReason::InvalidTrampolineForward);
49795003
}
@@ -6878,6 +6902,7 @@ where
68786902
fn process_pending_update_add_htlcs(&self) -> bool {
68796903
let mut should_persist = false;
68806904
let mut decode_update_add_htlcs = new_hash_map();
6905+
let mut dummy_update_add_htlcs = new_hash_map();
68816906
mem::swap(&mut decode_update_add_htlcs, &mut self.decode_update_add_htlcs.lock().unwrap());
68826907

68836908
let get_htlc_failure_type = |outgoing_scid_opt: Option<u64>, payment_hash: PaymentHash| {
@@ -6941,7 +6966,68 @@ where
69416966
&*self.logger,
69426967
&self.secp_ctx,
69436968
) {
6944-
Ok(decoded_onion) => decoded_onion,
6969+
Ok(decoded_onion) => match decoded_onion {
6970+
(
6971+
onion_utils::Hop::Dummy {
6972+
intro_node_blinding_point,
6973+
next_hop_hmac,
6974+
new_packet_bytes,
6975+
..
6976+
},
6977+
Some(NextPacketDetails {
6978+
next_packet_pubkey,
6979+
outgoing_connector,
6980+
..
6981+
}),
6982+
) => {
6983+
debug_assert!(
6984+
matches!(outgoing_connector, HopConnector::Dummy),
6985+
"Dummy hop must always map to HopConnector::Dummy"
6986+
);
6987+
6988+
// Dummy hops are not forwarded. Instead, we reconstruct a new UpdateAddHTLC
6989+
// with the next onion packet (ephemeral pubkey, hop data, and HMAC) and push
6990+
// it back into our own processing queue. This lets us step through the dummy
6991+
// layers locally until we reach the next real hop.
6992+
let next_blinding_point = intro_node_blinding_point
6993+
.or(update_add_htlc.blinding_point)
6994+
.and_then(|blinding_point| {
6995+
let ss = self
6996+
.node_signer
6997+
.ecdh(Recipient::Node, &blinding_point, None)
6998+
.ok()?
6999+
.secret_bytes();
7000+
7001+
onion_utils::next_hop_pubkey(
7002+
&self.secp_ctx,
7003+
blinding_point,
7004+
&ss,
7005+
)
7006+
.ok()
7007+
});
7008+
7009+
let new_onion_packet = OnionPacket {
7010+
version: 0,
7011+
public_key: next_packet_pubkey,
7012+
hop_data: new_packet_bytes,
7013+
hmac: next_hop_hmac,
7014+
};
7015+
7016+
let new_update_add_htlc = UpdateAddHTLC {
7017+
onion_routing_packet: new_onion_packet,
7018+
blinding_point: next_blinding_point,
7019+
..update_add_htlc.clone()
7020+
};
7021+
7022+
dummy_update_add_htlcs
7023+
.entry(incoming_scid_alias)
7024+
.or_insert_with(Vec::new)
7025+
.push(new_update_add_htlc);
7026+
7027+
continue;
7028+
},
7029+
_ => decoded_onion,
7030+
},
69457031

69467032
Err((htlc_fail, reason)) => {
69477033
let failure_type = HTLCHandlingFailureType::InvalidOnion;
@@ -6954,6 +7040,13 @@ where
69547040
let outgoing_scid_opt =
69557041
next_packet_details_opt.as_ref().and_then(|d| match d.outgoing_connector {
69567042
HopConnector::ShortChannelId(scid) => Some(scid),
7043+
HopConnector::Dummy => {
7044+
debug_assert!(
7045+
false,
7046+
"Dummy hops must never be processed at this stage."
7047+
);
7048+
None
7049+
},
69577050
HopConnector::Trampoline(_) => None,
69587051
});
69597052
let shared_secret = next_hop.shared_secret().secret_bytes();
@@ -7097,6 +7190,19 @@ where
70977190
));
70987191
}
70997192
}
7193+
7194+
// Merge peeled dummy HTLCs into the existing decode queue so they can be
7195+
// processed in the next iteration. We avoid replacing the whole queue
7196+
// (e.g. via mem::swap) because other threads may have enqueued new HTLCs
7197+
// meanwhile; merging preserves everything safely.
7198+
if !dummy_update_add_htlcs.is_empty() {
7199+
let mut decode_update_add_htlc_source = self.decode_update_add_htlcs.lock().unwrap();
7200+
7201+
for (incoming_scid_alias, htlcs) in dummy_update_add_htlcs.into_iter() {
7202+
decode_update_add_htlc_source.entry(incoming_scid_alias).or_default().extend(htlcs);
7203+
}
7204+
}
7205+
71007206
should_persist
71017207
}
71027208

@@ -11419,6 +11525,13 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
1141911525
for (forward_info, prev_htlc_id) in pending_forwards.drain(..) {
1142011526
let scid = match forward_info.routing {
1142111527
PendingHTLCRouting::Forward { short_channel_id, .. } => short_channel_id,
11528+
PendingHTLCRouting::Dummy { .. } => {
11529+
debug_assert!(
11530+
false,
11531+
"Dummy hops must never be processed at this stage."
11532+
);
11533+
0
11534+
},
1142211535
PendingHTLCRouting::TrampolineForward { .. } => 0,
1142311536
PendingHTLCRouting::Receive { .. } => 0,
1142411537
PendingHTLCRouting::ReceiveKeysend { .. } => 0,
@@ -15966,6 +16079,11 @@ impl_writeable_tlv_based_enum!(PendingHTLCRouting,
1596616079
(4, blinded, option),
1596716080
(6, node_id, required),
1596816081
(8, incoming_cltv_expiry, required),
16082+
},
16083+
(4, Dummy) => {
16084+
(0, onion_packet, required),
16085+
(1, blinded, required),
16086+
(2, incoming_cltv_expiry, option),
1596916087
}
1597016088
);
1597116089

lightning/src/ln/onion_payment.rs

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,10 @@ enum RoutingInfo {
106106
new_packet_bytes: [u8; ONION_DATA_LEN],
107107
next_hop_hmac: [u8; 32],
108108
},
109+
Dummy {
110+
new_packet_bytes: [u8; ONION_DATA_LEN],
111+
next_hop_hmac: [u8; 32],
112+
},
109113
Trampoline {
110114
next_trampoline: PublicKey,
111115
// Trampoline onions are currently variable length
@@ -149,13 +153,8 @@ pub(super) fn create_fwd_pending_htlc_info(
149153
(RoutingInfo::Direct { short_channel_id, new_packet_bytes, next_hop_hmac }, amt_to_forward, outgoing_cltv_value, intro_node_blinding_point,
150154
next_blinding_override)
151155
},
152-
onion_utils::Hop::Dummy { .. } => {
153-
debug_assert!(false, "Dummy hop should have been peeled earlier");
154-
return Err(InboundHTLCErr {
155-
msg: "Dummy Hop OnionHopData provided for us as an intermediary node",
156-
reason: LocalHTLCFailureReason::InvalidOnionPayload,
157-
err_data: Vec::new(),
158-
})
156+
onion_utils::Hop::Dummy { intro_node_blinding_point, next_hop_hmac, new_packet_bytes, .. } => {
157+
(RoutingInfo::Dummy { new_packet_bytes, next_hop_hmac }, msg.amount_msat, msg.cltv_expiry, intro_node_blinding_point, None)
159158
},
160159
onion_utils::Hop::Receive { .. } | onion_utils::Hop::BlindedReceive { .. } =>
161160
return Err(InboundHTLCErr {
@@ -234,7 +233,33 @@ pub(super) fn create_fwd_pending_htlc_info(
234233
.unwrap_or(BlindedFailure::FromBlindedNode),
235234
}),
236235
}
237-
}
236+
},
237+
RoutingInfo::Dummy { new_packet_bytes, next_hop_hmac } => {
238+
let outgoing_packet = msgs::OnionPacket {
239+
version: 0,
240+
public_key: next_packet_pubkey_opt.unwrap_or(Err(secp256k1::Error::InvalidPublicKey)),
241+
hop_data: new_packet_bytes,
242+
hmac: next_hop_hmac,
243+
};
244+
245+
PendingHTLCRouting::Dummy {
246+
onion_packet: outgoing_packet,
247+
incoming_cltv_expiry: Some(msg.cltv_expiry),
248+
blinded: {
249+
let inbound_blinding_point =
250+
intro_node_blinding_point.or(msg.blinding_point)
251+
.expect("Dummy HTLCs only occur within blinded paths");
252+
253+
BlindedForward {
254+
inbound_blinding_point,
255+
next_blinding_override: None,
256+
failure: intro_node_blinding_point
257+
.map(|_| BlindedFailure::FromIntroductionNode)
258+
.unwrap_or(BlindedFailure::FromBlindedNode),
259+
}
260+
},
261+
}
262+
},
238263
RoutingInfo::Trampoline { next_trampoline, new_packet_bytes, next_hop_hmac, shared_secret, current_path_key } => {
239264
let next_trampoline_packet_pubkey = match next_packet_pubkey_opt {
240265
Some(Ok(pubkey)) => pubkey,
@@ -545,6 +570,8 @@ where
545570
pub(super) enum HopConnector {
546571
// scid-based routing
547572
ShortChannelId(u64),
573+
// Dummy hop for path padding
574+
Dummy,
548575
// Trampoline-based routing
549576
#[allow(unused)]
550577
Trampoline(PublicKey),
@@ -648,7 +675,13 @@ where
648675
next_packet_pubkey, outgoing_connector: HopConnector::ShortChannelId(short_channel_id), outgoing_amt_msat: amt_to_forward,
649676
outgoing_cltv_value
650677
})
651-
}
678+
},
679+
onion_utils::Hop::Dummy { shared_secret, .. } => {
680+
let next_packet_pubkey = onion_utils::next_hop_pubkey(secp_ctx,
681+
msg.onion_routing_packet.public_key.unwrap(), &shared_secret.secret_bytes());
682+
683+
Some(NextPacketDetails { next_packet_pubkey, outgoing_connector: HopConnector::Dummy, outgoing_amt_msat: msg.amount_msat, outgoing_cltv_value: msg.cltv_expiry })
684+
},
652685
onion_utils::Hop::TrampolineForward { next_trampoline_hop_data: msgs::InboundTrampolineForwardPayload { amt_to_forward, outgoing_cltv_value, next_trampoline }, trampoline_shared_secret, incoming_trampoline_public_key, .. } => {
653686
let next_trampoline_packet_pubkey = onion_utils::next_hop_pubkey(secp_ctx,
654687
incoming_trampoline_public_key, &trampoline_shared_secret.secret_bytes());

lightning/src/ln/onion_utils.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2356,6 +2356,12 @@ where
23562356
new_packet_bytes,
23572357
})
23582358
},
2359+
msgs::InboundOnionPayload::Dummy { intro_node_blinding_point } => Ok(Hop::Dummy {
2360+
intro_node_blinding_point,
2361+
shared_secret,
2362+
next_hop_hmac,
2363+
new_packet_bytes,
2364+
}),
23592365
_ => {
23602366
if blinding_point.is_some() {
23612367
return Err(OnionDecodeErr::Malformed {

0 commit comments

Comments
 (0)