Skip to content

Commit 44f98c6

Browse files
shaavancodex
andcommitted
Expose blinded payment dummy-hop constructor
Make BlindedPaymentPath::new_with_dummy_hops a public API. Add end-to-end coverage for blinded payment paths with dummy hops before exposing the constructor. The updated tests now verify that dummy-hop relay fees are aggregated into payinfo, that the final claimable CLTV reflects the hidden dummy-hop deltas, and that an understated htlc_minimum_msat causes the payment to fail while the blinded path is processed. This replaces the previous TODO gate with concrete coverage for the behavior that callers depend on when constructing receive paths with dummy hops, making it reasonable to expose the API publicly. Co-Authored-By: OpenAI Codex <[email protected]>
1 parent 3649764 commit 44f98c6

2 files changed

Lines changed: 122 additions & 6 deletions

File tree

lightning/src/blinded_path/payment.rs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -136,10 +136,7 @@ impl BlindedPaymentPath {
136136
///
137137
/// This improves privacy by making path-length analysis based on fee and CLTV delta
138138
/// values less reliable.
139-
///
140-
/// TODO: Add end-to-end tests validating fee aggregation, CLTV deltas, and
141-
/// HTLC bounds when dummy hops are present, before exposing this API publicly.
142-
pub(crate) fn new_with_dummy_hops<
139+
pub fn new_with_dummy_hops<
143140
ES: EntropySource,
144141
T: secp256k1::Signing + secp256k1::Verification,
145142
>(

lightning/src/ln/blinded_payment_tests.rs

Lines changed: 121 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,30 @@ fn one_hop_blinded_path_with_dummy_hops() {
219219
payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext {}),
220220
};
221221
let receive_auth_key = chanmon_cfgs[1].keys_manager.get_receive_auth_key();
222-
let dummy_tlvs = [DummyTlvs::default(); 2];
222+
let dummy_tlvs = [
223+
DummyTlvs {
224+
payment_relay: PaymentRelay {
225+
cltv_expiry_delta: 21,
226+
fee_proportional_millionths: 0,
227+
fee_base_msat: 750,
228+
},
229+
payment_constraints: PaymentConstraints {
230+
max_cltv_expiry: u32::max_value(),
231+
htlc_minimum_msat: chan_upd.htlc_minimum_msat,
232+
},
233+
},
234+
DummyTlvs {
235+
payment_relay: PaymentRelay {
236+
cltv_expiry_delta: 33,
237+
fee_proportional_millionths: 0,
238+
fee_base_msat: 1_250,
239+
},
240+
payment_constraints: PaymentConstraints {
241+
max_cltv_expiry: u32::max_value(),
242+
htlc_minimum_msat: chan_upd.htlc_minimum_msat,
243+
},
244+
},
245+
];
223246

224247
let mut secp_ctx = Secp256k1::new();
225248
let blinded_path = BlindedPaymentPath::new_with_dummy_hops(
@@ -234,6 +257,9 @@ fn one_hop_blinded_path_with_dummy_hops() {
234257
&secp_ctx,
235258
)
236259
.unwrap();
260+
assert_eq!(blinded_path.payinfo.fee_base_msat, 2_000);
261+
assert_eq!(blinded_path.payinfo.fee_proportional_millionths, 0);
262+
assert_eq!(blinded_path.payinfo.cltv_expiry_delta, TEST_FINAL_CLTV as u16 + 21 + 33);
237263

238264
let route_params = RouteParameters::from_payment_params_and_value(
239265
PaymentParameters::blinded(vec![blinded_path]),
@@ -254,11 +280,14 @@ fn one_hop_blinded_path_with_dummy_hops() {
254280
let mut events = nodes[0].node.get_and_clear_pending_msg_events();
255281
assert_eq!(events.len(), 1);
256282
let ev = remove_first_msg_event_to_node(&nodes[1].node.get_our_node_id(), &mut events);
283+
let payment_event = SendEvent::from_event(ev.clone());
284+
let expected_claimable_cltv = payment_event.msgs[0].cltv_expiry - (21 + 33) as u32;
257285

258286
let path = &[&nodes[1]];
259287
let args = PassAlongPathArgs::new(&nodes[0], path, amt_msat, payment_hash, ev)
260288
.with_dummy_tlvs(&dummy_tlvs)
261-
.with_payment_secret(payment_secret);
289+
.with_payment_secret(payment_secret)
290+
.with_payment_claimable_cltv(expected_claimable_cltv);
262291

263292
do_pass_along_path(args);
264293
let path: &[&[&Node<'_, '_, '_>]] = &[&[&nodes[1]]];
@@ -359,6 +388,96 @@ fn one_hop_blinded_path_with_dummy_hops_underpaid() {
359388
);
360389
}
361390

391+
#[test]
392+
fn one_hop_blinded_path_with_dummy_hops_underadvertised_htlc_minimum_fails() {
393+
let chanmon_cfgs = create_chanmon_cfgs(2);
394+
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
395+
let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]);
396+
let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
397+
let chan_upd =
398+
create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0).0.contents;
399+
400+
let amt_msat = 5_000;
401+
let (_, payment_hash, payment_secret) =
402+
get_payment_preimage_hash(&nodes[1], Some(amt_msat), None);
403+
let payee_tlvs = ReceiveTlvs {
404+
payment_secret,
405+
payment_constraints: PaymentConstraints {
406+
max_cltv_expiry: u32::max_value(),
407+
htlc_minimum_msat: chan_upd.htlc_minimum_msat,
408+
},
409+
payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext {}),
410+
};
411+
let receive_auth_key = chanmon_cfgs[1].keys_manager.get_receive_auth_key();
412+
let dummy_tlvs = [DummyTlvs {
413+
payment_relay: PaymentRelay {
414+
cltv_expiry_delta: 18,
415+
fee_proportional_millionths: 0,
416+
fee_base_msat: 500,
417+
},
418+
payment_constraints: PaymentConstraints {
419+
max_cltv_expiry: u32::max_value(),
420+
htlc_minimum_msat: amt_msat + 2_000,
421+
},
422+
}];
423+
424+
let mut secp_ctx = Secp256k1::new();
425+
let blinded_path = BlindedPaymentPath::new_with_dummy_hops(
426+
&[],
427+
nodes[1].node.get_our_node_id(),
428+
&dummy_tlvs,
429+
receive_auth_key,
430+
payee_tlvs,
431+
u64::MAX,
432+
TEST_FINAL_CLTV as u16,
433+
&chanmon_cfgs[1].keys_manager,
434+
&secp_ctx,
435+
)
436+
.unwrap();
437+
assert!(blinded_path.payinfo.htlc_minimum_msat > amt_msat);
438+
439+
let mut route_params = RouteParameters::from_payment_params_and_value(
440+
PaymentParameters::blinded(vec![blinded_path]),
441+
amt_msat,
442+
);
443+
if let Payee::Blinded { ref mut route_hints, .. } = route_params.payment_params.payee {
444+
route_hints[0].payinfo.htlc_minimum_msat = amt_msat;
445+
} else {
446+
panic!();
447+
}
448+
449+
nodes[0]
450+
.node
451+
.send_payment(
452+
payment_hash,
453+
RecipientOnionFields::spontaneous_empty(amt_msat),
454+
PaymentId(payment_hash.0),
455+
route_params,
456+
Retry::Attempts(0),
457+
)
458+
.unwrap();
459+
check_added_monitors(&nodes[0], 1);
460+
461+
let mut events = nodes[0].node.get_and_clear_pending_msg_events();
462+
assert_eq!(events.len(), 1);
463+
let ev = remove_first_msg_event_to_node(&nodes[1].node.get_our_node_id(), &mut events);
464+
465+
let payment_event = SendEvent::from_event(ev);
466+
nodes[1].node.handle_update_add_htlc(nodes[0].node.get_our_node_id(), &payment_event.msgs[0]);
467+
check_added_monitors(&nodes[1], 0);
468+
do_commitment_signed_dance(&nodes[1], &nodes[0], &payment_event.commitment_msg, false, false);
469+
while nodes[1].node.needs_pending_htlc_processing() {
470+
nodes[1].node.process_pending_htlc_forwards();
471+
}
472+
expect_htlc_handling_failed_destinations!(
473+
nodes[1].node.get_and_clear_pending_events(),
474+
&[HTLCHandlingFailureType::InvalidOnion]
475+
);
476+
let mut updates = get_htlc_update_msgs(&nodes[1], &nodes[0].node.get_our_node_id());
477+
assert_eq!(updates.update_fail_htlcs.len() + updates.update_fail_malformed_htlcs.len(), 1);
478+
check_added_monitors(&nodes[1], 1);
479+
}
480+
362481
#[test]
363482
#[rustfmt::skip]
364483
fn mpp_to_one_hop_blinded_path() {

0 commit comments

Comments
 (0)