Skip to content

Commit df9928c

Browse files
committed
Introduce Dummy BlindedPaymentTlv
Dummy BlindedPaymentTlvs 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 de384ff commit df9928c

5 files changed

Lines changed: 146 additions & 21 deletions

File tree

lightning/src/blinded_path/payment.rs

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

331+
/// TLVs carried by a dummy hop within a blinded payment path.
332+
///
333+
/// Dummy hops do not correspond to real forwarding decisions, but are processed
334+
/// identically to real hops at the protocol level. The TLVs contained here define
335+
/// the relay requirements and constraints that must be satisfied for the payment
336+
/// to continue through this hop.
337+
///
338+
/// By enforcing realistic relay semantics on dummy hops, the payment path remains
339+
/// indistinguishable from a fully real route with respect to fees, CLTV deltas, and
340+
/// validation behavior.
341+
pub struct DummyTlvs {
342+
/// Relay requirements (fees and CLTV delta) that must be satisfied when
343+
/// processing this dummy hop.
344+
pub payment_relay: PaymentRelay,
345+
/// Constraints that apply to the payment when relaying over this dummy hop.
346+
pub payment_constraints: PaymentConstraints,
347+
}
348+
349+
impl Default for DummyTlvs {
350+
fn default() -> Self {
351+
let payment_relay =
352+
PaymentRelay { cltv_expiry_delta: 0, fee_proportional_millionths: 0, fee_base_msat: 0 };
353+
354+
let payment_constraints =
355+
PaymentConstraints { max_cltv_expiry: u32::MAX, htlc_minimum_msat: 0 };
356+
357+
Self { payment_relay, payment_constraints }
358+
}
359+
}
360+
331361
/// Data to construct a [`BlindedHop`] for receiving a payment. This payload is custom to LDK and
332362
/// may not be valid if received by another lightning implementation.
333363
#[derive(Clone, Debug)]
@@ -346,6 +376,8 @@ pub struct ReceiveTlvs {
346376
pub(crate) enum BlindedPaymentTlvs {
347377
/// This blinded payment data is for a forwarding node.
348378
Forward(ForwardTlvs),
379+
/// This blinded payment data is dummy and is to be peeled by receiving node.
380+
Dummy(DummyTlvs),
349381
/// This blinded payment data is for the receiving node.
350382
Receive(ReceiveTlvs),
351383
}
@@ -363,6 +395,7 @@ pub(crate) enum BlindedTrampolineTlvs {
363395
// Used to include forward and receive TLVs in the same iterator for encoding.
364396
enum BlindedPaymentTlvsRef<'a> {
365397
Forward(&'a ForwardTlvs),
398+
Dummy(&'a DummyTlvs),
366399
Receive(&'a ReceiveTlvs),
367400
}
368401

@@ -512,6 +545,17 @@ impl Writeable for TrampolineForwardTlvs {
512545
}
513546
}
514547

548+
impl Writeable for DummyTlvs {
549+
fn write<W: Writer>(&self, w: &mut W) -> Result<(), io::Error> {
550+
encode_tlv_stream!(w, {
551+
(10, self.payment_relay, required),
552+
(12, self.payment_constraints, required),
553+
(65539, (), required),
554+
});
555+
Ok(())
556+
}
557+
}
558+
515559
// Note: The `authentication` TLV field was removed in LDK v0.3 following
516560
// the introduction of `ReceiveAuthKey`-based authentication for inbound
517561
// `BlindedPaymentPaths`s. Because we do not support receiving to those
@@ -532,6 +576,7 @@ impl<'a> Writeable for BlindedPaymentTlvsRef<'a> {
532576
fn write<W: Writer>(&self, w: &mut W) -> Result<(), io::Error> {
533577
match self {
534578
Self::Forward(tlvs) => tlvs.write(w)?,
579+
Self::Dummy(tlvs) => tlvs.write(w)?,
535580
Self::Receive(tlvs) => tlvs.write(w)?,
536581
}
537582
Ok(())
@@ -552,28 +597,41 @@ impl Readable for BlindedPaymentTlvs {
552597
(14, features, (option, encoding: (BlindedHopFeatures, WithoutLength))),
553598
(65536, payment_secret, option),
554599
(65537, payment_context, option),
600+
(65539, is_dummy, option)
555601
});
556602

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 {
562-
short_channel_id,
563-
payment_relay: payment_relay.ok_or(DecodeError::InvalidValue)?,
564-
payment_constraints: payment_constraints.0.unwrap(),
565-
next_blinding_override,
566-
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-
}))
603+
match (
604+
scid,
605+
next_blinding_override,
606+
payment_relay,
607+
features,
608+
payment_secret,
609+
payment_context,
610+
is_dummy,
611+
) {
612+
(Some(short_channel_id), next_override, Some(relay), features, None, None, None) => {
613+
Ok(BlindedPaymentTlvs::Forward(ForwardTlvs {
614+
short_channel_id,
615+
payment_relay: relay,
616+
payment_constraints: payment_constraints.0.unwrap(),
617+
next_blinding_override: next_override,
618+
features: features.unwrap_or_else(BlindedHopFeatures::empty),
619+
}))
620+
},
621+
(None, None, None, None, Some(secret), Some(context), None) => {
622+
Ok(BlindedPaymentTlvs::Receive(ReceiveTlvs {
623+
payment_secret: secret,
624+
payment_constraints: payment_constraints.0.unwrap(),
625+
payment_context: context,
626+
}))
627+
},
628+
(None, None, Some(relay), None, None, None, Some(())) => {
629+
Ok(BlindedPaymentTlvs::Dummy(DummyTlvs {
630+
payment_relay: relay,
631+
payment_constraints: payment_constraints.0.unwrap(),
632+
}))
633+
},
634+
_ => return Err(DecodeError::InvalidValue),
577635
}
578636
}
579637
}

lightning/src/ln/channelmanager.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5105,6 +5105,20 @@ where
51055105
onion_utils::Hop::Forward { .. } | onion_utils::Hop::BlindedForward { .. } => {
51065106
create_fwd_pending_htlc_info(msg, decoded_hop, shared_secret, next_packet_pubkey_opt)
51075107
},
5108+
onion_utils::Hop::Dummy { .. } => {
5109+
debug_assert!(
5110+
false,
5111+
"Reached unreachable dummy-hop HTLC. Dummy hops are peeled in \
5112+
`process_pending_update_add_htlcs`, and the resulting HTLC is \
5113+
re-enqueued for processing. Hitting this means the peel-and-requeue \
5114+
step was missed."
5115+
);
5116+
return Err(InboundHTLCErr {
5117+
msg: "Failed to decode update add htlc onion",
5118+
reason: LocalHTLCFailureReason::InvalidOnionPayload,
5119+
err_data: Vec::new(),
5120+
})
5121+
},
51085122
onion_utils::Hop::TrampolineForward { .. } | onion_utils::Hop::TrampolineBlindedForward { .. } => {
51095123
create_fwd_pending_htlc_info(msg, decoded_hop, shared_secret, next_packet_pubkey_opt)
51105124
},

lightning/src/ln/msgs.rs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ use bitcoin::secp256k1::PublicKey;
3232
use bitcoin::{secp256k1, Transaction, Witness};
3333

3434
use crate::blinded_path::message::BlindedMessagePath;
35-
use crate::blinded_path::payment::{BlindedPaymentTlvs, ForwardTlvs, ReceiveTlvs};
35+
use crate::blinded_path::payment::{BlindedPaymentTlvs, DummyTlvs, ForwardTlvs, ReceiveTlvs};
3636
use crate::blinded_path::payment::{BlindedTrampolineTlvs, TrampolineForwardTlvs};
3737
use crate::ln::onion_utils;
3838
use crate::ln::types::ChannelId;
@@ -2336,6 +2336,11 @@ mod fuzzy_internal_msgs {
23362336
pub intro_node_blinding_point: Option<PublicKey>,
23372337
pub next_blinding_override: Option<PublicKey>,
23382338
}
2339+
pub struct InboundOnionDummyPayload {
2340+
pub payment_relay: PaymentRelay,
2341+
pub payment_constraints: PaymentConstraints,
2342+
pub intro_node_blinding_point: Option<PublicKey>,
2343+
}
23392344
pub struct InboundOnionBlindedReceivePayload {
23402345
pub sender_intended_htlc_amt_msat: u64,
23412346
pub total_msat: u64,
@@ -2355,6 +2360,7 @@ mod fuzzy_internal_msgs {
23552360
Receive(InboundOnionReceivePayload),
23562361
BlindedForward(InboundOnionBlindedForwardPayload),
23572362
BlindedReceive(InboundOnionBlindedReceivePayload),
2363+
Dummy(InboundOnionDummyPayload),
23582364
}
23592365

23602366
pub struct InboundTrampolineForwardPayload {
@@ -3694,6 +3700,25 @@ where
36943700
next_blinding_override,
36953701
}))
36963702
},
3703+
ChaChaDualPolyReadAdapter {
3704+
readable:
3705+
BlindedPaymentTlvs::Dummy(DummyTlvs { payment_relay, payment_constraints }),
3706+
used_aad,
3707+
} => {
3708+
if amt.is_some()
3709+
|| cltv_value.is_some() || total_msat.is_some()
3710+
|| keysend_preimage.is_some()
3711+
|| invoice_request.is_some()
3712+
|| !used_aad
3713+
{
3714+
return Err(DecodeError::InvalidValue);
3715+
}
3716+
Ok(Self::Dummy(InboundOnionDummyPayload {
3717+
payment_relay,
3718+
payment_constraints,
3719+
intro_node_blinding_point,
3720+
}))
3721+
},
36973722
ChaChaDualPolyReadAdapter {
36983723
readable: BlindedPaymentTlvs::Receive(receive_tlvs),
36993724
used_aad,

lightning/src/ln/onion_payment.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,14 @@ pub(super) fn create_fwd_pending_htlc_info(
149149
(RoutingInfo::Direct { short_channel_id, new_packet_bytes, next_hop_hmac }, amt_to_forward, outgoing_cltv_value, intro_node_blinding_point,
150150
next_blinding_override)
151151
},
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+
})
159+
},
152160
onion_utils::Hop::Receive { .. } | onion_utils::Hop::BlindedReceive { .. } =>
153161
return Err(InboundHTLCErr {
154162
msg: "Final Node OnionHopData provided for us as an intermediary node",
@@ -364,6 +372,14 @@ pub(super) fn create_recv_pending_htlc_info(
364372
msg: "Got blinded non final data with an HMAC of 0",
365373
})
366374
},
375+
onion_utils::Hop::Dummy { .. } => {
376+
debug_assert!(false, "Dummy hop should have been peeled earlier");
377+
return Err(InboundHTLCErr {
378+
reason: LocalHTLCFailureReason::InvalidOnionBlinding,
379+
err_data: vec![0; 32],
380+
msg: "Got blinded non final data with an HMAC of 0",
381+
})
382+
}
367383
onion_utils::Hop::TrampolineForward { .. } | onion_utils::Hop::TrampolineBlindedForward { .. } => {
368384
return Err(InboundHTLCErr {
369385
reason: LocalHTLCFailureReason::InvalidOnionPayload,

lightning/src/ln/onion_utils.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2223,6 +2223,17 @@ pub(crate) enum Hop {
22232223
/// Bytes of the onion packet we're forwarding.
22242224
new_packet_bytes: [u8; ONION_DATA_LEN],
22252225
},
2226+
/// This onion payload is dummy, and needs to be peeled by us.
2227+
Dummy {
2228+
/// Blinding point for introduction-node dummy hops.
2229+
dummy_hop_data: msgs::InboundOnionDummyPayload,
2230+
/// Shared secret for decrypting the next-hop public key.
2231+
shared_secret: SharedSecret,
2232+
/// HMAC of the next hop's onion packet.
2233+
next_hop_hmac: [u8; 32],
2234+
/// Onion packet bytes after this dummy layer is peeled.
2235+
new_packet_bytes: [u8; ONION_DATA_LEN],
2236+
},
22262237
/// This onion payload was for us, not for forwarding to a next-hop. Contains information for
22272238
/// verifying the incoming payment.
22282239
Receive {
@@ -2277,6 +2288,7 @@ impl Hop {
22772288
match self {
22782289
Hop::Forward { shared_secret, .. } => shared_secret,
22792290
Hop::BlindedForward { shared_secret, .. } => shared_secret,
2291+
Hop::Dummy { shared_secret, .. } => shared_secret,
22802292
Hop::TrampolineForward { outer_shared_secret, .. } => outer_shared_secret,
22812293
Hop::TrampolineBlindedForward { outer_shared_secret, .. } => outer_shared_secret,
22822294
Hop::Receive { shared_secret, .. } => shared_secret,

0 commit comments

Comments
 (0)