Skip to content

Commit e8a0a76

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 e8a0a76

File tree

2 files changed

+73
-19
lines changed

2 files changed

+73
-19
lines changed

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/msgs.rs

Lines changed: 14 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 { payment_tlvs_authenticated: bool },
23512352
}
23522353

23532354
pub struct InboundTrampolineForwardPayload {
@@ -3689,6 +3690,19 @@ 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 { payment_tlvs_authenticated: used_aad })
3705+
},
36923706
ChaChaDualPolyReadAdapter {
36933707
readable: BlindedPaymentTlvs::Receive(receive_tlvs),
36943708
used_aad,

0 commit comments

Comments
 (0)