Skip to content

Commit 879ac54

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 a93113c commit 879ac54

5 files changed

Lines changed: 112 additions & 19 deletions

File tree

lightning/src/blinded_path/payment.rs

Lines changed: 59 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,15 @@ pub struct TrampolineForwardTlvs {
322322
pub next_blinding_override: Option<PublicKey>,
323323
}
324324

325+
/// Represents the dummy TLV encoded immediately before the actual [`ReceiveTlvs`] in a blinded path.
326+
/// These TLVs are intended for the final node and are recursively authenticated until the real
327+
/// [`ReceiveTlvs`] is reached.
328+
///
329+
/// Their purpose is to arbitrarily extend the path length, obscuring the receiver's position in the
330+
/// route and thereby enhancing privacy.
331+
#[derive(Debug)]
332+
pub(crate) struct PaymentDummyTlv;
333+
325334
/// Data to construct a [`BlindedHop`] for receiving a payment. This payload is custom to LDK and
326335
/// may not be valid if received by another lightning implementation.
327336
#[derive(Clone, Debug)]
@@ -340,6 +349,8 @@ pub struct ReceiveTlvs {
340349
pub(crate) enum BlindedPaymentTlvs {
341350
/// This blinded payment data is for a forwarding node.
342351
Forward(ForwardTlvs),
352+
/// This blinded payment data is dummy and is to be peeled by receiving node.
353+
Dummy(PaymentDummyTlv),
343354
/// This blinded payment data is for the receiving node.
344355
Receive(ReceiveTlvs),
345356
}
@@ -357,6 +368,7 @@ pub(crate) enum BlindedTrampolineTlvs {
357368
// Used to include forward and receive TLVs in the same iterator for encoding.
358369
enum BlindedPaymentTlvsRef<'a> {
359370
Forward(&'a ForwardTlvs),
371+
Dummy(&'a PaymentDummyTlv),
360372
Receive(&'a ReceiveTlvs),
361373
}
362374

@@ -506,6 +518,15 @@ impl Writeable for TrampolineForwardTlvs {
506518
}
507519
}
508520

521+
impl Writeable for PaymentDummyTlv {
522+
fn write<W: Writer>(&self, writer: &mut W) -> Result<(), io::Error> {
523+
encode_tlv_stream!(writer, {
524+
(65539, (), required),
525+
});
526+
Ok(())
527+
}
528+
}
529+
509530
// Note: Authentication TLV field was removed in LDK v0.2 following the
510531
// introduction of `ReceiveAuthKey`-based authentication for inbound
511532
// `BlindedPaymentPaths`s. Because we do not support receiving to those
@@ -526,6 +547,7 @@ impl<'a> Writeable for BlindedPaymentTlvsRef<'a> {
526547
fn write<W: Writer>(&self, w: &mut W) -> Result<(), io::Error> {
527548
match self {
528549
Self::Forward(tlvs) => tlvs.write(w)?,
550+
Self::Dummy(tlv) => tlv.write(w)?,
529551
Self::Receive(tlvs) => tlvs.write(w)?,
530552
}
531553
Ok(())
@@ -542,32 +564,50 @@ impl Readable for BlindedPaymentTlvs {
542564
(2, scid, option),
543565
(8, next_blinding_override, option),
544566
(10, payment_relay, option),
545-
(12, payment_constraints, required),
567+
(12, payment_constraints, option),
546568
(14, features, (option, encoding: (BlindedHopFeatures, WithoutLength))),
547569
(65536, payment_secret, option),
548570
(65537, payment_context, option),
571+
(65539, is_dummy, option)
549572
});
550573

551-
if let Some(short_channel_id) = scid {
552-
if payment_secret.is_some() {
553-
return Err(DecodeError::InvalidValue);
554-
}
555-
Ok(BlindedPaymentTlvs::Forward(ForwardTlvs {
574+
match (
575+
scid,
576+
next_blinding_override,
577+
payment_relay,
578+
payment_constraints,
579+
features,
580+
payment_secret,
581+
payment_context,
582+
is_dummy,
583+
) {
584+
(
585+
Some(short_channel_id),
586+
next_override,
587+
Some(relay),
588+
Some(constraints),
589+
features,
590+
None,
591+
None,
592+
None,
593+
) => Ok(BlindedPaymentTlvs::Forward(ForwardTlvs {
556594
short_channel_id,
557-
payment_relay: payment_relay.ok_or(DecodeError::InvalidValue)?,
558-
payment_constraints: payment_constraints.0.unwrap(),
559-
next_blinding_override,
595+
payment_relay: relay,
596+
payment_constraints: constraints,
597+
next_blinding_override: next_override,
560598
features: features.unwrap_or_else(BlindedHopFeatures::empty),
561-
}))
562-
} else {
563-
if payment_relay.is_some() || features.is_some() {
564-
return Err(DecodeError::InvalidValue);
565-
}
566-
Ok(BlindedPaymentTlvs::Receive(ReceiveTlvs {
567-
payment_secret: payment_secret.ok_or(DecodeError::InvalidValue)?,
568-
payment_constraints: payment_constraints.0.unwrap(),
569-
payment_context: payment_context.ok_or(DecodeError::InvalidValue)?,
570-
}))
599+
})),
600+
(None, None, None, Some(constraints), None, Some(secret), Some(context), None) => {
601+
Ok(BlindedPaymentTlvs::Receive(ReceiveTlvs {
602+
payment_secret: secret,
603+
payment_constraints: constraints,
604+
payment_context: context,
605+
}))
606+
},
607+
(None, None, None, None, None, None, None, Some(())) => {
608+
Ok(BlindedPaymentTlvs::Dummy(PaymentDummyTlv))
609+
},
610+
_ => return Err(DecodeError::InvalidValue),
571611
}
572612
}
573613
}

lightning/src/ln/channelmanager.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4886,6 +4886,14 @@ where
48864886
onion_utils::Hop::Forward { .. } | onion_utils::Hop::BlindedForward { .. } => {
48874887
create_fwd_pending_htlc_info(msg, decoded_hop, shared_secret, next_packet_pubkey_opt)
48884888
},
4889+
onion_utils::Hop::Dummy { .. } => {
4890+
debug_assert!(false, "Shouldn't be triggered.");
4891+
return Err(InboundHTLCErr {
4892+
msg: "Failed to decode update add htlc onion",
4893+
reason: LocalHTLCFailureReason::InvalidOnionPayload,
4894+
err_data: Vec::new(),
4895+
})
4896+
},
48894897
onion_utils::Hop::TrampolineForward { .. } | onion_utils::Hop::TrampolineBlindedForward { .. } => {
48904898
create_fwd_pending_htlc_info(msg, decoded_hop, shared_secret, next_packet_pubkey_opt)
48914899
},

lightning/src/ln/msgs.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2348,6 +2348,7 @@ mod fuzzy_internal_msgs {
23482348
Receive(InboundOnionReceivePayload),
23492349
BlindedForward(InboundOnionBlindedForwardPayload),
23502350
BlindedReceive(InboundOnionBlindedReceivePayload),
2351+
Dummy { intro_node_blinding_point: Option<PublicKey>, payment_tlvs_authenticated: bool },
23512352
}
23522353

23532354
pub struct InboundTrampolineForwardPayload {
@@ -3689,6 +3690,22 @@ where
36893690
next_blinding_override,
36903691
}))
36913692
},
3693+
ChaChaDualPolyReadAdapter {
3694+
readable: BlindedPaymentTlvs::Dummy(_dummy_tlv),
3695+
used_aad,
3696+
} => {
3697+
if amt.is_some()
3698+
|| cltv_value.is_some() || total_msat.is_some()
3699+
|| keysend_preimage.is_some()
3700+
|| invoice_request.is_some()
3701+
{
3702+
return Err(DecodeError::InvalidValue);
3703+
}
3704+
Ok(Self::Dummy {
3705+
intro_node_blinding_point,
3706+
payment_tlvs_authenticated: used_aad,
3707+
})
3708+
},
36923709
ChaChaDualPolyReadAdapter {
36933710
readable: BlindedPaymentTlvs::Receive(receive_tlvs),
36943711
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",
@@ -344,6 +352,14 @@ where
344352
msg: "Got blinded non final data with an HMAC of 0",
345353
})
346354
},
355+
onion_utils::Hop::Dummy { .. } => {
356+
debug_assert!(false, "This case shall not be triggered.");
357+
return Err(InboundHTLCErr {
358+
reason: LocalHTLCFailureReason::InvalidOnionBlinding,
359+
err_data: vec![0; 32],
360+
msg: "Got blinded non final data with an HMAC of 0",
361+
})
362+
}
347363
onion_utils::Hop::TrampolineForward { .. } | onion_utils::Hop::TrampolineBlindedForward { .. } => {
348364
return Err(InboundHTLCErr {
349365
reason: LocalHTLCFailureReason::InvalidOnionPayload,

lightning/src/ln/onion_utils.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2228,6 +2228,17 @@ pub(crate) enum Hop {
22282228
/// Bytes of the onion packet we're forwarding.
22292229
new_packet_bytes: [u8; ONION_DATA_LEN],
22302230
},
2231+
/// This onion payload is dummy, and needs to be peeled by us.
2232+
Dummy {
2233+
/// Onion payload data used in interpreting the dummy hop
2234+
intro_node_blinding_point: Option<PublicKey>,
2235+
/// Shared secret that was used to decrypt next_hop_data.
2236+
shared_secret: SharedSecret,
2237+
/// HMAC of the next hop's onion packet.
2238+
next_hop_hmac: [u8; 32],
2239+
/// Bytes of the onion packet we're forwarding.
2240+
new_packet_bytes: [u8; ONION_DATA_LEN],
2241+
},
22312242
/// This onion payload was for us, not for forwarding to a next-hop. Contains information for
22322243
/// verifying the incoming payment.
22332244
Receive {
@@ -2284,6 +2295,7 @@ impl Hop {
22842295
match self {
22852296
Hop::Forward { shared_secret, .. } => shared_secret,
22862297
Hop::BlindedForward { shared_secret, .. } => shared_secret,
2298+
Hop::Dummy { shared_secret, .. } => shared_secret,
22872299
Hop::TrampolineForward { outer_shared_secret, .. } => outer_shared_secret,
22882300
Hop::TrampolineBlindedForward { outer_shared_secret, .. } => outer_shared_secret,
22892301
Hop::Receive { shared_secret, .. } => shared_secret,

0 commit comments

Comments
 (0)