Skip to content

Commit a81b31b

Browse files
committed
Introduce PaymentDummyTlv
PaymentDummyTlv is an empty TLV inserted immediately before the actual ReceiveTlvs in a blinded path. Receivers treat these dummy hops as real hops, which prevents timing-based attacks. Allowing arbitrary dummy hops before the final ReceiveTlvs obscures the recipient's true position in the route and makes it harder for an onlooker to infer the destination, strengthening recipient privacy.
1 parent 6d4897c commit a81b31b

File tree

5 files changed

+110
-19
lines changed

5 files changed

+110
-19
lines changed

lightning/src/blinded_path/payment.rs

Lines changed: 59 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,15 @@ pub struct TrampolineForwardTlvs {
328328
pub next_blinding_override: Option<PublicKey>,
329329
}
330330

331+
/// Represents the dummy TLV encoded immediately before the actual [`ReceiveTlvs`] in a blinded path.
332+
/// These TLVs are intended for the final node and are recursively authenticated until the real
333+
/// [`ReceiveTlvs`] is reached.
334+
///
335+
/// Their purpose is to arbitrarily extend the path length, obscuring the receiver's position in the
336+
/// route and thereby enhancing privacy.
337+
#[derive(Debug)]
338+
pub(crate) struct PaymentDummyTlv;
339+
331340
/// Data to construct a [`BlindedHop`] for receiving a payment. This payload is custom to LDK and
332341
/// may not be valid if received by another lightning implementation.
333342
#[derive(Clone, Debug)]
@@ -346,6 +355,8 @@ pub struct ReceiveTlvs {
346355
pub(crate) enum BlindedPaymentTlvs {
347356
/// This blinded payment data is for a forwarding node.
348357
Forward(ForwardTlvs),
358+
/// This blinded payment data is dummy and is to be peeled by receiving node.
359+
Dummy(PaymentDummyTlv),
349360
/// This blinded payment data is for the receiving node.
350361
Receive(ReceiveTlvs),
351362
}
@@ -363,6 +374,7 @@ pub(crate) enum BlindedTrampolineTlvs {
363374
// Used to include forward and receive TLVs in the same iterator for encoding.
364375
enum BlindedPaymentTlvsRef<'a> {
365376
Forward(&'a ForwardTlvs),
377+
Dummy(&'a PaymentDummyTlv),
366378
Receive(&'a ReceiveTlvs),
367379
}
368380

@@ -512,6 +524,15 @@ impl Writeable for TrampolineForwardTlvs {
512524
}
513525
}
514526

527+
impl Writeable for PaymentDummyTlv {
528+
fn write<W: Writer>(&self, writer: &mut W) -> Result<(), io::Error> {
529+
encode_tlv_stream!(writer, {
530+
(65539, (), required),
531+
});
532+
Ok(())
533+
}
534+
}
535+
515536
// Note: The `authentication` TLV field was removed in LDK v0.3 following
516537
// the introduction of `ReceiveAuthKey`-based authentication for inbound
517538
// `BlindedPaymentPaths`s. Because we do not support receiving to those
@@ -532,6 +553,7 @@ impl<'a> Writeable for BlindedPaymentTlvsRef<'a> {
532553
fn write<W: Writer>(&self, w: &mut W) -> Result<(), io::Error> {
533554
match self {
534555
Self::Forward(tlvs) => tlvs.write(w)?,
556+
Self::Dummy(tlv) => tlv.write(w)?,
535557
Self::Receive(tlvs) => tlvs.write(w)?,
536558
}
537559
Ok(())
@@ -548,32 +570,50 @@ impl Readable for BlindedPaymentTlvs {
548570
(2, scid, option),
549571
(8, next_blinding_override, option),
550572
(10, payment_relay, option),
551-
(12, payment_constraints, required),
573+
(12, payment_constraints, option),
552574
(14, features, (option, encoding: (BlindedHopFeatures, WithoutLength))),
553575
(65536, payment_secret, option),
554576
(65537, payment_context, option),
577+
(65539, is_dummy, option)
555578
});
556579

557-
if let Some(short_channel_id) = scid {
558-
if payment_secret.is_some() {
559-
return Err(DecodeError::InvalidValue);
560-
}
561-
Ok(BlindedPaymentTlvs::Forward(ForwardTlvs {
580+
match (
581+
scid,
582+
next_blinding_override,
583+
payment_relay,
584+
payment_constraints,
585+
features,
586+
payment_secret,
587+
payment_context,
588+
is_dummy,
589+
) {
590+
(
591+
Some(short_channel_id),
592+
next_override,
593+
Some(relay),
594+
Some(constraints),
595+
features,
596+
None,
597+
None,
598+
None,
599+
) => Ok(BlindedPaymentTlvs::Forward(ForwardTlvs {
562600
short_channel_id,
563-
payment_relay: payment_relay.ok_or(DecodeError::InvalidValue)?,
564-
payment_constraints: payment_constraints.0.unwrap(),
565-
next_blinding_override,
601+
payment_relay: relay,
602+
payment_constraints: constraints,
603+
next_blinding_override: next_override,
566604
features: features.unwrap_or_else(BlindedHopFeatures::empty),
567-
}))
568-
} else {
569-
if payment_relay.is_some() || features.is_some() {
570-
return Err(DecodeError::InvalidValue);
571-
}
572-
Ok(BlindedPaymentTlvs::Receive(ReceiveTlvs {
573-
payment_secret: payment_secret.ok_or(DecodeError::InvalidValue)?,
574-
payment_constraints: payment_constraints.0.unwrap(),
575-
payment_context: payment_context.ok_or(DecodeError::InvalidValue)?,
576-
}))
605+
})),
606+
(None, None, None, Some(constraints), None, Some(secret), Some(context), None) => {
607+
Ok(BlindedPaymentTlvs::Receive(ReceiveTlvs {
608+
payment_secret: secret,
609+
payment_constraints: constraints,
610+
payment_context: context,
611+
}))
612+
},
613+
(None, None, None, None, None, None, None, Some(())) => {
614+
Ok(BlindedPaymentTlvs::Dummy(PaymentDummyTlv))
615+
},
616+
_ => return Err(DecodeError::InvalidValue),
577617
}
578618
}
579619
}

lightning/src/ln/channelmanager.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5089,6 +5089,14 @@ where
50895089
onion_utils::Hop::Forward { .. } | onion_utils::Hop::BlindedForward { .. } => {
50905090
create_fwd_pending_htlc_info(msg, decoded_hop, shared_secret, next_packet_pubkey_opt)
50915091
},
5092+
onion_utils::Hop::Dummy { .. } => {
5093+
debug_assert!(false, "Shouldn't be triggered.");
5094+
return Err(InboundHTLCErr {
5095+
msg: "Failed to decode update add htlc onion",
5096+
reason: LocalHTLCFailureReason::InvalidOnionPayload,
5097+
err_data: Vec::new(),
5098+
})
5099+
},
50925100
onion_utils::Hop::TrampolineForward { .. } | onion_utils::Hop::TrampolineBlindedForward { .. } => {
50935101
create_fwd_pending_htlc_info(msg, decoded_hop, shared_secret, next_packet_pubkey_opt)
50945102
},

lightning/src/ln/msgs.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2355,6 +2355,7 @@ mod fuzzy_internal_msgs {
23552355
Receive(InboundOnionReceivePayload),
23562356
BlindedForward(InboundOnionBlindedForwardPayload),
23572357
BlindedReceive(InboundOnionBlindedReceivePayload),
2358+
Dummy { intro_node_blinding_point: Option<PublicKey> },
23582359
}
23592360

23602361
pub struct InboundTrampolineForwardPayload {
@@ -3694,6 +3695,20 @@ where
36943695
next_blinding_override,
36953696
}))
36963697
},
3698+
ChaChaDualPolyReadAdapter {
3699+
readable: BlindedPaymentTlvs::Dummy(_dummy_tlv),
3700+
used_aad,
3701+
} => {
3702+
if amt.is_some()
3703+
|| cltv_value.is_some() || total_msat.is_some()
3704+
|| keysend_preimage.is_some()
3705+
|| invoice_request.is_some()
3706+
|| !used_aad
3707+
{
3708+
return Err(DecodeError::InvalidValue);
3709+
}
3710+
Ok(Self::Dummy { intro_node_blinding_point })
3711+
},
36973712
ChaChaDualPolyReadAdapter {
36983713
readable: BlindedPaymentTlvs::Receive(receive_tlvs),
36993714
used_aad,

lightning/src/ln/onion_payment.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,14 @@ pub(super) fn create_fwd_pending_htlc_info(
123123
(RoutingInfo::Direct { short_channel_id, new_packet_bytes, next_hop_hmac }, amt_to_forward, outgoing_cltv_value, intro_node_blinding_point,
124124
next_blinding_override)
125125
},
126+
onion_utils::Hop::Dummy { .. } => {
127+
debug_assert!(false, "This case shall not be triggered");
128+
return Err(InboundHTLCErr {
129+
msg: "Dummy Hop OnionHopData provided for us as an intermediary node",
130+
reason: LocalHTLCFailureReason::InvalidOnionPayload,
131+
err_data: Vec::new(),
132+
})
133+
},
126134
onion_utils::Hop::Receive { .. } | onion_utils::Hop::BlindedReceive { .. } =>
127135
return Err(InboundHTLCErr {
128136
msg: "Final Node OnionHopData provided for us as an intermediary node",
@@ -327,6 +335,14 @@ pub(super) fn create_recv_pending_htlc_info(
327335
msg: "Got blinded non final data with an HMAC of 0",
328336
})
329337
},
338+
onion_utils::Hop::Dummy { .. } => {
339+
debug_assert!(false, "This case shall not be triggered.");
340+
return Err(InboundHTLCErr {
341+
reason: LocalHTLCFailureReason::InvalidOnionBlinding,
342+
err_data: vec![0; 32],
343+
msg: "Got blinded non final data with an HMAC of 0",
344+
})
345+
}
330346
onion_utils::Hop::TrampolineForward { .. } | onion_utils::Hop::TrampolineBlindedForward { .. } => {
331347
return Err(InboundHTLCErr {
332348
reason: LocalHTLCFailureReason::InvalidOnionPayload,

lightning/src/ln/onion_utils.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2202,6 +2202,17 @@ pub(crate) enum Hop {
22022202
/// Bytes of the onion packet we're forwarding.
22032203
new_packet_bytes: [u8; ONION_DATA_LEN],
22042204
},
2205+
/// This onion payload is dummy, and needs to be peeled by us.
2206+
Dummy {
2207+
/// Onion payload data used in interpreting the dummy hop
2208+
intro_node_blinding_point: Option<PublicKey>,
2209+
/// Shared secret that was used to decrypt next_hop_data.
2210+
shared_secret: SharedSecret,
2211+
/// HMAC of the next hop's onion packet.
2212+
next_hop_hmac: [u8; 32],
2213+
/// Bytes of the onion packet we're forwarding.
2214+
new_packet_bytes: [u8; ONION_DATA_LEN],
2215+
},
22052216
/// This onion payload was for us, not for forwarding to a next-hop. Contains information for
22062217
/// verifying the incoming payment.
22072218
Receive {
@@ -2256,6 +2267,7 @@ impl Hop {
22562267
match self {
22572268
Hop::Forward { shared_secret, .. } => shared_secret,
22582269
Hop::BlindedForward { shared_secret, .. } => shared_secret,
2270+
Hop::Dummy { shared_secret, .. } => shared_secret,
22592271
Hop::TrampolineForward { outer_shared_secret, .. } => outer_shared_secret,
22602272
Hop::TrampolineBlindedForward { outer_shared_secret, .. } => outer_shared_secret,
22612273
Hop::Receive { shared_secret, .. } => shared_secret,

0 commit comments

Comments
 (0)