Skip to content

Commit e5b172c

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 6d4897c commit e5b172c

File tree

5 files changed

+97
-19
lines changed

5 files changed

+97
-19
lines changed

lightning/src/blinded_path/payment.rs

Lines changed: 43 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,8 @@ pub struct ReceiveTlvs {
346346
pub(crate) enum BlindedPaymentTlvs {
347347
/// This blinded payment data is for a forwarding node.
348348
Forward(ForwardTlvs),
349+
/// This blinded payment data is dummy and is to be peeled by receiving node.
350+
Dummy,
349351
/// This blinded payment data is for the receiving node.
350352
Receive(ReceiveTlvs),
351353
}
@@ -363,6 +365,7 @@ pub(crate) enum BlindedTrampolineTlvs {
363365
// Used to include forward and receive TLVs in the same iterator for encoding.
364366
enum BlindedPaymentTlvsRef<'a> {
365367
Forward(&'a ForwardTlvs),
368+
Dummy,
366369
Receive(&'a ReceiveTlvs),
367370
}
368371

@@ -532,6 +535,11 @@ impl<'a> Writeable for BlindedPaymentTlvsRef<'a> {
532535
fn write<W: Writer>(&self, w: &mut W) -> Result<(), io::Error> {
533536
match self {
534537
Self::Forward(tlvs) => tlvs.write(w)?,
538+
Self::Dummy => {
539+
encode_tlv_stream!(w, {
540+
(65539, (), required),
541+
})
542+
},
535543
Self::Receive(tlvs) => tlvs.write(w)?,
536544
}
537545
Ok(())
@@ -548,32 +556,48 @@ impl Readable for BlindedPaymentTlvs {
548556
(2, scid, option),
549557
(8, next_blinding_override, option),
550558
(10, payment_relay, option),
551-
(12, payment_constraints, required),
559+
(12, payment_constraints, option),
552560
(14, features, (option, encoding: (BlindedHopFeatures, WithoutLength))),
553561
(65536, payment_secret, option),
554562
(65537, payment_context, option),
563+
(65539, is_dummy, option)
555564
});
556565

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 {
566+
match (
567+
scid,
568+
next_blinding_override,
569+
payment_relay,
570+
payment_constraints,
571+
features,
572+
payment_secret,
573+
payment_context,
574+
is_dummy,
575+
) {
576+
(
577+
Some(short_channel_id),
578+
next_override,
579+
Some(relay),
580+
Some(constraints),
581+
features,
582+
None,
583+
None,
584+
None,
585+
) => Ok(BlindedPaymentTlvs::Forward(ForwardTlvs {
562586
short_channel_id,
563-
payment_relay: payment_relay.ok_or(DecodeError::InvalidValue)?,
564-
payment_constraints: payment_constraints.0.unwrap(),
565-
next_blinding_override,
587+
payment_relay: relay,
588+
payment_constraints: constraints,
589+
next_blinding_override: next_override,
566590
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-
}))
591+
})),
592+
(None, None, None, Some(constraints), None, Some(secret), Some(context), None) => {
593+
Ok(BlindedPaymentTlvs::Receive(ReceiveTlvs {
594+
payment_secret: secret,
595+
payment_constraints: constraints,
596+
payment_context: context,
597+
}))
598+
},
599+
(None, None, None, None, None, None, None, Some(())) => Ok(BlindedPaymentTlvs::Dummy),
600+
_ => return Err(DecodeError::InvalidValue),
577601
}
578602
}
579603
}

lightning/src/ln/channelmanager.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5089,6 +5089,20 @@ 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!(
5094+
false,
5095+
"Reached unreachable dummy-hop HTLC. Dummy hops are peeled in \
5096+
`process_pending_update_add_htlcs`, and the resulting HTLC is \
5097+
re-enqueued for processing. Hitting this means the peel-and-requeue \
5098+
step was missed."
5099+
);
5100+
return Err(InboundHTLCErr {
5101+
msg: "Failed to decode update add htlc onion",
5102+
reason: LocalHTLCFailureReason::InvalidOnionPayload,
5103+
err_data: Vec::new(),
5104+
})
5105+
},
50925106
onion_utils::Hop::TrampolineForward { .. } | onion_utils::Hop::TrampolineBlindedForward { .. } => {
50935107
create_fwd_pending_htlc_info(msg, decoded_hop, shared_secret, next_packet_pubkey_opt)
50945108
},

lightning/src/ln/msgs.rs

Lines changed: 12 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,17 @@ where
36943695
next_blinding_override,
36953696
}))
36963697
},
3698+
ChaChaDualPolyReadAdapter { readable: BlindedPaymentTlvs::Dummy, used_aad } => {
3699+
if amt.is_some()
3700+
|| cltv_value.is_some() || total_msat.is_some()
3701+
|| keysend_preimage.is_some()
3702+
|| invoice_request.is_some()
3703+
|| !used_aad
3704+
{
3705+
return Err(DecodeError::InvalidValue);
3706+
}
3707+
Ok(Self::Dummy { intro_node_blinding_point })
3708+
},
36973709
ChaChaDualPolyReadAdapter {
36983710
readable: BlindedPaymentTlvs::Receive(receive_tlvs),
36993711
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, "Dummy hop should have been peeled earlier");
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, "Dummy hop should have been peeled earlier");
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+
/// Blinding point for introduction-node dummy hops.
2208+
intro_node_blinding_point: Option<PublicKey>,
2209+
/// Shared secret for decrypting the next-hop public key.
2210+
shared_secret: SharedSecret,
2211+
/// HMAC of the next hop's onion packet.
2212+
next_hop_hmac: [u8; 32],
2213+
/// Onion packet bytes after this dummy layer is peeled.
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)