Skip to content

Commit 4ce34ba

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 4ce34ba

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
@@ -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: 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
@@ -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+
intro_node_blinding_point: Option<PublicKey>,
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)