Skip to content

Commit 01916d3

Browse files
committed
Introduce Payment Dummy Hop parsing mechanism
1 parent d94e950 commit 01916d3

4 files changed

Lines changed: 238 additions & 31 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)]
@@ -265,28 +264,31 @@ impl BlindedPaymentPath {
265264
NL::Target: NodeIdLookUp,
266265
T: secp256k1::Signing + secp256k1::Verification,
267266
{
268-
match self.decrypt_intro_payload::<NS>(node_signer) {
269-
Ok((
270-
BlindedPaymentTlvs::Forward(ForwardTlvs { short_channel_id, .. }),
271-
control_tlvs_ss,
272-
)) => {
273-
let next_node_id = match node_id_lookup.next_node_id(short_channel_id) {
274-
Some(node_id) => node_id,
275-
None => return Err(()),
276-
};
277-
let mut new_blinding_point = onion_utils::next_hop_pubkey(
278-
secp_ctx,
279-
self.inner_path.blinding_point,
280-
control_tlvs_ss.as_ref(),
281-
)
282-
.map_err(|_| ())?;
283-
mem::swap(&mut self.inner_path.blinding_point, &mut new_blinding_point);
284-
self.inner_path.introduction_node = IntroductionNode::NodeId(next_node_id);
285-
self.inner_path.blinded_hops.remove(0);
286-
Ok(())
287-
},
288-
_ => Err(()),
289-
}
267+
let (next_node_id, control_tlvs_ss) =
268+
match self.decrypt_intro_payload::<NS>(node_signer).map_err(|_| ())? {
269+
(BlindedPaymentTlvs::Forward(ForwardTlvs { short_channel_id, .. }), ss) => {
270+
let node_id = node_id_lookup.next_node_id(short_channel_id).ok_or(())?;
271+
(node_id, ss)
272+
},
273+
(BlindedPaymentTlvs::Dummy(_), ss) => {
274+
let node_id = node_signer.get_node_id(Recipient::Node)?;
275+
(node_id, ss)
276+
},
277+
_ => return Err(()),
278+
};
279+
280+
let new_blinding_point = onion_utils::next_hop_pubkey(
281+
secp_ctx,
282+
self.inner_path.blinding_point,
283+
control_tlvs_ss.as_ref(),
284+
)
285+
.map_err(|_| ())?;
286+
287+
self.inner_path.blinding_point = new_blinding_point;
288+
self.inner_path.introduction_node = IntroductionNode::NodeId(next_node_id);
289+
self.inner_path.blinded_hops.remove(0);
290+
291+
Ok(())
290292
}
291293

292294
pub(crate) fn decrypt_intro_payload<NS: Deref>(
@@ -308,9 +310,9 @@ impl BlindedPaymentPath {
308310
.map_err(|_| ())?;
309311

310312
match (&readable, used_aad) {
311-
(BlindedPaymentTlvs::Forward(_), false) | (BlindedPaymentTlvs::Receive(_), true) => {
312-
Ok((readable, control_tlvs_ss))
313-
},
313+
(BlindedPaymentTlvs::Forward(_), false)
314+
| (BlindedPaymentTlvs::Dummy(_), true)
315+
| (BlindedPaymentTlvs::Receive(_), true) => Ok((readable, control_tlvs_ss)),
314316
_ => Err(()),
315317
}
316318
}

lightning/src/ln/channelmanager.rs

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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,36 @@ 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+
dummy_hop_data,
6973+
next_hop_hmac,
6974+
new_packet_bytes,
6975+
..
6976+
},
6977+
Some(next_packet_details),
6978+
) => {
6979+
let new_update_add_htlc =
6980+
onion_utils::peel_dummy_hop_update_add_htlc(
6981+
update_add_htlc,
6982+
dummy_hop_data,
6983+
next_hop_hmac,
6984+
new_packet_bytes,
6985+
next_packet_details,
6986+
&*self.node_signer,
6987+
&self.secp_ctx,
6988+
);
6989+
6990+
dummy_update_add_htlcs
6991+
.entry(incoming_scid_alias)
6992+
.or_insert_with(Vec::new)
6993+
.push(new_update_add_htlc);
6994+
6995+
continue;
6996+
},
6997+
_ => decoded_onion,
6998+
},
69456999

69467000
Err((htlc_fail, reason)) => {
69477001
let failure_type = HTLCHandlingFailureType::InvalidOnion;
@@ -6954,6 +7008,13 @@ where
69547008
let outgoing_scid_opt =
69557009
next_packet_details_opt.as_ref().and_then(|d| match d.outgoing_connector {
69567010
HopConnector::ShortChannelId(scid) => Some(scid),
7011+
HopConnector::Dummy => {
7012+
debug_assert!(
7013+
false,
7014+
"Dummy hops must never be processed at this stage."
7015+
);
7016+
None
7017+
},
69577018
HopConnector::Trampoline(_) => None,
69587019
});
69597020
let shared_secret = next_hop.shared_secret().secret_bytes();
@@ -7097,6 +7158,19 @@ where
70977158
));
70987159
}
70997160
}
7161+
7162+
// Merge peeled dummy HTLCs into the existing decode queue so they can be
7163+
// processed in the next iteration. We avoid replacing the whole queue
7164+
// (e.g. via mem::swap) because other threads may have enqueued new HTLCs
7165+
// meanwhile; merging preserves everything safely.
7166+
if !dummy_update_add_htlcs.is_empty() {
7167+
let mut decode_update_add_htlc_source = self.decode_update_add_htlcs.lock().unwrap();
7168+
7169+
for (incoming_scid_alias, htlcs) in dummy_update_add_htlcs.into_iter() {
7170+
decode_update_add_htlc_source.entry(incoming_scid_alias).or_default().extend(htlcs);
7171+
}
7172+
}
7173+
71007174
should_persist
71017175
}
71027176

@@ -11419,6 +11493,13 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
1141911493
for (forward_info, prev_htlc_id) in pending_forwards.drain(..) {
1142011494
let scid = match forward_info.routing {
1142111495
PendingHTLCRouting::Forward { short_channel_id, .. } => short_channel_id,
11496+
PendingHTLCRouting::Dummy { .. } => {
11497+
debug_assert!(
11498+
false,
11499+
"Dummy hops must never be processed at this stage."
11500+
);
11501+
0
11502+
},
1142211503
PendingHTLCRouting::TrampolineForward { .. } => 0,
1142311504
PendingHTLCRouting::Receive { .. } => 0,
1142411505
PendingHTLCRouting::ReceiveKeysend { .. } => 0,
@@ -15966,6 +16047,11 @@ impl_writeable_tlv_based_enum!(PendingHTLCRouting,
1596616047
(4, blinded, option),
1596716048
(6, node_id, required),
1596816049
(8, incoming_cltv_expiry, required),
16050+
},
16051+
(4, Dummy) => {
16052+
(0, onion_packet, required),
16053+
(1, blinded, required),
16054+
(2, incoming_cltv_expiry, option),
1596916055
}
1597016056
);
1597116057

lightning/src/ln/onion_payment.rs

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,22 @@ fn check_blinded_forward(
7474
Ok((amt_to_forward, outgoing_cltv_value))
7575
}
7676

77+
#[rustfmt::skip]
78+
fn check_dummy_forward(
79+
inbound_amt_msat: u64, inbound_cltv_expiry: u32, payment_relay: &PaymentRelay,
80+
payment_constraints: &PaymentConstraints,
81+
) -> Result<(u64, u32), ()> {
82+
let amt_to_forward = blinded_path::payment::amt_to_forward_msat(
83+
inbound_amt_msat, payment_relay
84+
).ok_or(())?;
85+
let outgoing_cltv_value = inbound_cltv_expiry.checked_sub(
86+
payment_relay.cltv_expiry_delta as u32
87+
).ok_or(())?;
88+
check_blinded_payment_constraints(inbound_amt_msat, outgoing_cltv_value, payment_constraints)?;
89+
90+
Ok((amt_to_forward, outgoing_cltv_value))
91+
}
92+
7793
fn check_trampoline_payment_constraints(
7894
outer_hop_data: &msgs::InboundTrampolineEntrypointPayload, trampoline_cltv_value: u32,
7995
trampoline_amount: u64,
@@ -234,7 +250,7 @@ pub(super) fn create_fwd_pending_htlc_info(
234250
.unwrap_or(BlindedFailure::FromBlindedNode),
235251
}),
236252
}
237-
}
253+
},
238254
RoutingInfo::Trampoline { next_trampoline, new_packet_bytes, next_hop_hmac, shared_secret, current_path_key } => {
239255
let next_trampoline_packet_pubkey = match next_packet_pubkey_opt {
240256
Some(Ok(pubkey)) => pubkey,
@@ -494,7 +510,7 @@ where
494510
L::Target: Logger,
495511
{
496512
let (hop, next_packet_details_opt) =
497-
decode_incoming_update_add_htlc_onion(msg, node_signer, logger, secp_ctx
513+
decode_incoming_update_add_htlc_onion(msg, &*node_signer, &*logger, secp_ctx
498514
).map_err(|(msg, failure_reason)| {
499515
let (reason, err_data) = match msg {
500516
HTLCFailureMsg::Malformed(_) => (failure_reason, Vec::new()),
@@ -532,6 +548,29 @@ where
532548
// onion here and check it.
533549
create_fwd_pending_htlc_info(msg, hop, shared_secret.secret_bytes(), Some(next_packet_pubkey))?
534550
},
551+
onion_utils::Hop::Dummy { dummy_hop_data, next_hop_hmac, new_packet_bytes, .. } => {
552+
let next_packet_details = match next_packet_details_opt {
553+
Some(next_packet_details) => next_packet_details,
554+
// Dummy Hops should always include the next hop details
555+
None => return Err(InboundHTLCErr {
556+
msg: "Failed to decode update add htlc onion",
557+
reason: LocalHTLCFailureReason::InvalidOnionPayload,
558+
err_data: Vec::new(),
559+
}),
560+
};
561+
562+
let new_update_add_htlc = onion_utils::peel_dummy_hop_update_add_htlc(
563+
msg,
564+
dummy_hop_data,
565+
next_hop_hmac,
566+
new_packet_bytes,
567+
next_packet_details,
568+
&*node_signer,
569+
secp_ctx
570+
);
571+
572+
peel_payment_onion(&new_update_add_htlc, node_signer, logger, secp_ctx, cur_height, allow_skimmed_fees)?
573+
},
535574
_ => {
536575
let shared_secret = hop.shared_secret().secret_bytes();
537576
create_recv_pending_htlc_info(
@@ -545,6 +584,8 @@ where
545584
pub(super) enum HopConnector {
546585
// scid-based routing
547586
ShortChannelId(u64),
587+
// Dummy hop for path padding
588+
Dummy,
548589
// Trampoline-based routing
549590
#[allow(unused)]
550591
Trampoline(PublicKey),
@@ -648,7 +689,23 @@ where
648689
next_packet_pubkey, outgoing_connector: HopConnector::ShortChannelId(short_channel_id), outgoing_amt_msat: amt_to_forward,
649690
outgoing_cltv_value
650691
})
651-
}
692+
},
693+
onion_utils::Hop::Dummy { dummy_hop_data: msgs::InboundOnionDummyPayload { ref payment_relay, ref payment_constraints, .. }, shared_secret, .. } => {
694+
let (amt_to_forward, outgoing_cltv_value) = match check_dummy_forward(
695+
msg.amount_msat, msg.cltv_expiry, &payment_relay, &payment_constraints,
696+
) {
697+
Ok((amt, cltv)) => (amt, cltv),
698+
Err(()) => {
699+
return encode_relay_error("Underflow calculating outbound amount or cltv value for blinded forward",
700+
LocalHTLCFailureReason::InvalidOnionBlinding, shared_secret.secret_bytes(), None, &[0; 32]);
701+
}
702+
};
703+
704+
let next_packet_pubkey = onion_utils::next_hop_pubkey(secp_ctx,
705+
msg.onion_routing_packet.public_key.unwrap(), &shared_secret.secret_bytes());
706+
707+
Some(NextPacketDetails { next_packet_pubkey, outgoing_connector: HopConnector::Dummy, outgoing_amt_msat: amt_to_forward, outgoing_cltv_value })
708+
},
652709
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, .. } => {
653710
let next_trampoline_packet_pubkey = onion_utils::next_hop_pubkey(secp_ctx,
654711
incoming_trampoline_public_key, &trampoline_shared_secret.secret_bytes());

0 commit comments

Comments
 (0)