Skip to content

Commit d94e950

Browse files
committed
Introduce Dummy Hop support in Blinded Path Constructor
Adds a new constructor for blinded paths that allows specifying the number of dummy hops. This enables users to insert arbitrary hops before the real destination, enhancing privacy by making it harder to infer the sender–receiver distance or identify the final destination. Lays the groundwork for future use of dummy hops in blinded path construction.
1 parent df9928c commit d94e950

1 file changed

Lines changed: 154 additions & 13 deletions

File tree

lightning/src/blinded_path/payment.rs

Lines changed: 154 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use bitcoin::secp256k1::ecdh::SharedSecret;
1313
use bitcoin::secp256k1::{self, PublicKey, Secp256k1, SecretKey};
1414

15+
use crate::blinded_path::message::MAX_DUMMY_HOPS_COUNT;
1516
use crate::blinded_path::utils::{self, BlindedPathWithPadding};
1617
use crate::blinded_path::{BlindedHop, BlindedPath, IntroductionNode, NodeIdLookUp};
1718
use crate::crypto::streams::ChaChaDualPolyReadAdapter;
@@ -124,6 +125,74 @@ impl BlindedPaymentPath {
124125
where
125126
ES::Target: EntropySource,
126127
{
128+
BlindedPaymentPath::new_inner(
129+
intermediate_nodes,
130+
payee_node_id,
131+
local_node_receive_key,
132+
0,
133+
None,
134+
payee_tlvs,
135+
htlc_maximum_msat,
136+
min_final_cltv_expiry_delta,
137+
entropy_source,
138+
secp_ctx,
139+
)
140+
}
141+
142+
/// Same as [`BlindedPaymentPath::new`], but allows specifying a number of dummy hops.
143+
///
144+
/// Dummy TLVs allow callers to override the payment relay values used for dummy hops.
145+
/// Any additional fees introduced by these dummy hops are ultimately paid to the final
146+
/// recipient as part of the total amount.
147+
///
148+
/// This improves privacy by making path-length analysis based on fee and CLTV delta
149+
/// values less reliable.
150+
///
151+
/// Note:
152+
/// At most [`MAX_DUMMY_HOPS_COUNT`] dummy hops can be added to the blinded path.
153+
pub fn new_with_dummy_hops<ES: Deref, T: secp256k1::Signing + secp256k1::Verification>(
154+
intermediate_nodes: &[PaymentForwardNode], payee_node_id: PublicKey,
155+
dummy_hop_count: usize, local_node_receive_key: ReceiveAuthKey, payee_tlvs: ReceiveTlvs,
156+
htlc_maximum_msat: u64, min_final_cltv_expiry_delta: u16, entropy_source: ES,
157+
secp_ctx: &Secp256k1<T>,
158+
) -> Result<Self, ()>
159+
where
160+
ES::Target: EntropySource,
161+
{
162+
// TODO: Expose DummyTlvs in the public API instead of always using defaults.
163+
let dummy_tlvs = DummyTlvs::default();
164+
165+
BlindedPaymentPath::new_inner(
166+
intermediate_nodes,
167+
payee_node_id,
168+
local_node_receive_key,
169+
dummy_hop_count,
170+
Some(dummy_tlvs),
171+
payee_tlvs,
172+
htlc_maximum_msat,
173+
min_final_cltv_expiry_delta,
174+
entropy_source,
175+
secp_ctx,
176+
)
177+
}
178+
179+
fn new_inner<ES: Deref, T: secp256k1::Signing + secp256k1::Verification>(
180+
intermediate_nodes: &[PaymentForwardNode], payee_node_id: PublicKey,
181+
local_node_receive_key: ReceiveAuthKey, dummy_hop_count: usize,
182+
dummy_tlvs_opt: Option<DummyTlvs>, payee_tlvs: ReceiveTlvs, htlc_maximum_msat: u64,
183+
min_final_cltv_expiry_delta: u16, entropy_source: ES, secp_ctx: &Secp256k1<T>,
184+
) -> Result<Self, ()>
185+
where
186+
ES::Target: EntropySource,
187+
{
188+
debug_assert!(
189+
(dummy_hop_count > 0) == dummy_tlvs_opt.is_some(),
190+
"dummy_hop_count and dummy_tlvs must either both be present or both be absent"
191+
);
192+
193+
let dummy_count = core::cmp::min(dummy_hop_count, MAX_DUMMY_HOPS_COUNT);
194+
let dummy_tlvs = dummy_tlvs_opt.unwrap_or_default();
195+
127196
let introduction_node = IntroductionNode::NodeId(
128197
intermediate_nodes.first().map_or(payee_node_id, |n| n.node_id),
129198
);
@@ -133,10 +202,13 @@ impl BlindedPaymentPath {
133202

134203
let blinded_payinfo = compute_payinfo(
135204
intermediate_nodes,
205+
dummy_count,
206+
&dummy_tlvs,
136207
&payee_tlvs,
137208
htlc_maximum_msat,
138209
min_final_cltv_expiry_delta,
139210
)?;
211+
140212
Ok(Self {
141213
inner_path: BlindedPath {
142214
introduction_node,
@@ -145,6 +217,8 @@ impl BlindedPaymentPath {
145217
secp_ctx,
146218
intermediate_nodes,
147219
payee_node_id,
220+
dummy_count,
221+
dummy_tlvs,
148222
payee_tlvs,
149223
&blinding_secret,
150224
local_node_receive_key,
@@ -393,6 +467,7 @@ pub(crate) enum BlindedTrampolineTlvs {
393467
}
394468

395469
// Used to include forward and receive TLVs in the same iterator for encoding.
470+
#[derive(Clone)]
396471
enum BlindedPaymentTlvsRef<'a> {
397472
Forward(&'a ForwardTlvs),
398473
Dummy(&'a DummyTlvs),
@@ -678,21 +753,46 @@ pub(crate) const PAYMENT_PADDING_ROUND_OFF: usize = 30;
678753
/// Construct blinded payment hops for the given `intermediate_nodes` and payee info.
679754
pub(super) fn blinded_hops<T: secp256k1::Signing + secp256k1::Verification>(
680755
secp_ctx: &Secp256k1<T>, intermediate_nodes: &[PaymentForwardNode], payee_node_id: PublicKey,
681-
payee_tlvs: ReceiveTlvs, session_priv: &SecretKey, local_node_receive_key: ReceiveAuthKey,
756+
dummy_count: usize, dummy_tlvs: DummyTlvs, payee_tlvs: ReceiveTlvs, session_priv: &SecretKey,
757+
local_node_receive_key: ReceiveAuthKey,
682758
) -> Vec<BlindedHop> {
683759
let pks = intermediate_nodes
684760
.iter()
685761
.map(|node| (node.node_id, None))
762+
.chain(core::iter::repeat((payee_node_id, Some(local_node_receive_key))).take(dummy_count))
686763
.chain(core::iter::once((payee_node_id, Some(local_node_receive_key))));
687764
let tlvs = intermediate_nodes
688765
.iter()
689766
.map(|node| BlindedPaymentTlvsRef::Forward(&node.tlvs))
767+
.chain(core::iter::repeat(BlindedPaymentTlvsRef::Dummy(&dummy_tlvs)).take(dummy_count))
690768
.chain(core::iter::once(BlindedPaymentTlvsRef::Receive(&payee_tlvs)));
691769

692770
let path = pks.zip(
693771
tlvs.map(|tlv| BlindedPathWithPadding { tlvs: tlv, round_off: PAYMENT_PADDING_ROUND_OFF }),
694772
);
695773

774+
// Debug invariant: all non-final hops must have identical serialized size.
775+
#[cfg(debug_assertions)]
776+
{
777+
let mut iter = path.clone();
778+
if let Some((_, first)) = iter.next() {
779+
let remaining = iter.clone().count(); // includes intermediate + final
780+
781+
// At least one intermediate hop
782+
if remaining > 1 {
783+
let expected = first.serialized_length();
784+
785+
// skip final hop: take(remaining - 1)
786+
for (_, hop) in iter.take(remaining - 1) {
787+
debug_assert!(
788+
hop.serialized_length() == expected,
789+
"All intermediate blinded hops must have identical serialized size"
790+
);
791+
}
792+
}
793+
}
794+
}
795+
696796
utils::construct_blinded_hops(secp_ctx, path, session_priv)
697797
}
698798

@@ -752,14 +852,24 @@ where
752852
}
753853

754854
pub(super) fn compute_payinfo(
755-
intermediate_nodes: &[PaymentForwardNode], payee_tlvs: &ReceiveTlvs,
756-
payee_htlc_maximum_msat: u64, min_final_cltv_expiry_delta: u16,
855+
intermediate_nodes: &[PaymentForwardNode], dummy_count: usize, dummy_tlvs: &DummyTlvs,
856+
payee_tlvs: &ReceiveTlvs, payee_htlc_maximum_msat: u64, min_final_cltv_expiry_delta: u16,
757857
) -> Result<BlindedPayInfo, ()> {
758-
let (aggregated_base_fee, aggregated_prop_fee) =
759-
compute_aggregated_base_prop_fee(intermediate_nodes.iter().map(|node| RoutingFees {
858+
let dummy_tlvs_iter = core::iter::repeat(dummy_tlvs).take(dummy_count);
859+
860+
let routing_fees = intermediate_nodes
861+
.iter()
862+
.map(|node| RoutingFees {
760863
base_msat: node.tlvs.payment_relay.fee_base_msat,
761864
proportional_millionths: node.tlvs.payment_relay.fee_proportional_millionths,
762-
}))?;
865+
})
866+
.chain(dummy_tlvs_iter.clone().map(|tlvs| RoutingFees {
867+
base_msat: tlvs.payment_relay.fee_base_msat,
868+
proportional_millionths: tlvs.payment_relay.fee_proportional_millionths,
869+
}));
870+
871+
let (aggregated_base_fee, aggregated_prop_fee) =
872+
compute_aggregated_base_prop_fee(routing_fees)?;
763873

764874
let mut htlc_minimum_msat: u64 = 1;
765875
let mut htlc_maximum_msat: u64 = 21_000_000 * 100_000_000 * 1_000; // Total bitcoin supply
@@ -788,6 +898,16 @@ pub(super) fn compute_payinfo(
788898
)
789899
.ok_or(())?; // If underflow occurs, we cannot send to this hop without exceeding their max
790900
}
901+
for dummy_tlvs in dummy_tlvs_iter {
902+
cltv_expiry_delta =
903+
cltv_expiry_delta.checked_add(dummy_tlvs.payment_relay.cltv_expiry_delta).ok_or(())?;
904+
905+
htlc_minimum_msat = amt_to_forward_msat(
906+
core::cmp::max(dummy_tlvs.payment_constraints.htlc_minimum_msat, htlc_minimum_msat),
907+
&dummy_tlvs.payment_relay,
908+
)
909+
.unwrap_or(1); // If underflow occurs, we definitely reached this node's min
910+
}
791911
htlc_minimum_msat =
792912
core::cmp::max(payee_tlvs.payment_constraints.htlc_minimum_msat, htlc_minimum_msat);
793913
htlc_maximum_msat = core::cmp::min(payee_htlc_maximum_msat, htlc_maximum_msat);
@@ -874,8 +994,8 @@ impl_writeable_tlv_based!(Bolt12RefundContext, {});
874994
#[cfg(test)]
875995
mod tests {
876996
use crate::blinded_path::payment::{
877-
Bolt12RefundContext, ForwardTlvs, PaymentConstraints, PaymentContext, PaymentForwardNode,
878-
PaymentRelay, ReceiveTlvs,
997+
Bolt12RefundContext, DummyTlvs, ForwardTlvs, PaymentConstraints, PaymentContext,
998+
PaymentForwardNode, PaymentRelay, ReceiveTlvs,
879999
};
8801000
use crate::ln::functional_test_utils::TEST_FINAL_CLTV;
8811001
use crate::types::features::BlindedHopFeatures;
@@ -931,9 +1051,15 @@ mod tests {
9311051
payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext {}),
9321052
};
9331053
let htlc_maximum_msat = 100_000;
934-
let blinded_payinfo =
935-
super::compute_payinfo(&intermediate_nodes[..], &recv_tlvs, htlc_maximum_msat, 12)
936-
.unwrap();
1054+
let blinded_payinfo = super::compute_payinfo(
1055+
&intermediate_nodes[..],
1056+
0,
1057+
&DummyTlvs::default(),
1058+
&recv_tlvs,
1059+
htlc_maximum_msat,
1060+
12,
1061+
)
1062+
.unwrap();
9371063
assert_eq!(blinded_payinfo.fee_base_msat, 201);
9381064
assert_eq!(blinded_payinfo.fee_proportional_millionths, 1001);
9391065
assert_eq!(blinded_payinfo.cltv_expiry_delta, 300);
@@ -948,8 +1074,15 @@ mod tests {
9481074
payment_constraints: PaymentConstraints { max_cltv_expiry: 0, htlc_minimum_msat: 1 },
9491075
payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext {}),
9501076
};
951-
let blinded_payinfo =
952-
super::compute_payinfo(&[], &recv_tlvs, 4242, TEST_FINAL_CLTV as u16).unwrap();
1077+
let blinded_payinfo = super::compute_payinfo(
1078+
&[],
1079+
0,
1080+
&DummyTlvs::default(),
1081+
&recv_tlvs,
1082+
4242,
1083+
TEST_FINAL_CLTV as u16,
1084+
)
1085+
.unwrap();
9531086
assert_eq!(blinded_payinfo.fee_base_msat, 0);
9541087
assert_eq!(blinded_payinfo.fee_proportional_millionths, 0);
9551088
assert_eq!(blinded_payinfo.cltv_expiry_delta, TEST_FINAL_CLTV as u16);
@@ -1008,6 +1141,8 @@ mod tests {
10081141
let htlc_maximum_msat = 100_000;
10091142
let blinded_payinfo = super::compute_payinfo(
10101143
&intermediate_nodes[..],
1144+
0,
1145+
&DummyTlvs::default(),
10111146
&recv_tlvs,
10121147
htlc_maximum_msat,
10131148
TEST_FINAL_CLTV as u16,
@@ -1067,6 +1202,8 @@ mod tests {
10671202
let htlc_minimum_msat = 3798;
10681203
assert!(super::compute_payinfo(
10691204
&intermediate_nodes[..],
1205+
0,
1206+
&DummyTlvs::default(),
10701207
&recv_tlvs,
10711208
htlc_minimum_msat - 1,
10721209
TEST_FINAL_CLTV as u16
@@ -1076,6 +1213,8 @@ mod tests {
10761213
let htlc_maximum_msat = htlc_minimum_msat + 1;
10771214
let blinded_payinfo = super::compute_payinfo(
10781215
&intermediate_nodes[..],
1216+
0,
1217+
&DummyTlvs::default(),
10791218
&recv_tlvs,
10801219
htlc_maximum_msat,
10811220
TEST_FINAL_CLTV as u16,
@@ -1136,6 +1275,8 @@ mod tests {
11361275

11371276
let blinded_payinfo = super::compute_payinfo(
11381277
&intermediate_nodes[..],
1278+
0,
1279+
&DummyTlvs::default(),
11391280
&recv_tlvs,
11401281
10_000,
11411282
TEST_FINAL_CLTV as u16,

0 commit comments

Comments
 (0)