Skip to content

Commit a6e6676

Browse files
authored
Merge pull request #4402 from TheBlueMatt/2026-02-trampoline-cltv-consistency
Use HTLC CLTV instead of onion CLTV values for payment claim timer
2 parents 14e522f + e39437d commit a6e6676

7 files changed

Lines changed: 156 additions & 91 deletions

File tree

lightning/src/ln/blinded_payment_tests.rs

Lines changed: 46 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -981,23 +981,19 @@ fn do_multi_hop_receiver_fail(check: ReceiveCheckFail) {
981981
};
982982

983983
let amt_msat = 5000;
984-
let excess_final_cltv_delta_opt = if check == ReceiveCheckFail::ProcessPendingHTLCsCheck {
985-
// Set the final CLTV expiry too low to trigger the failure in process_pending_htlc_forwards.
986-
Some(TEST_FINAL_CLTV as u16 - 2)
984+
let required_final_cltv = if check == ReceiveCheckFail::ProcessPendingHTLCsCheck {
985+
// Set the final CLTV required much too high to trigger the failure in process_pending_htlc_forwards.
986+
Some((TEST_FINAL_CLTV as u16) * 10)
987987
} else { None };
988-
let (_, payment_hash, payment_secret) = get_payment_preimage_hash(&nodes[2], Some(amt_msat), excess_final_cltv_delta_opt);
988+
let (_, payment_hash, payment_secret) = get_payment_preimage_hash(&nodes[2], Some(amt_msat), required_final_cltv);
989989
let mut route_params = get_blinded_route_parameters(amt_msat, payment_secret, 1, 1_0000_0000,
990990
nodes.iter().skip(1).map(|n| n.node.get_our_node_id()).collect(), &[&chan_upd_1_2],
991991
&chanmon_cfgs[2].keys_manager);
992992

993993
route_params.payment_params.max_path_length = 17;
994994

995995
let route = if check == ReceiveCheckFail::ProcessPendingHTLCsCheck {
996-
let mut route = get_route(&nodes[0], &route_params).unwrap();
997-
// Set the final CLTV expiry too low to trigger the failure in process_pending_htlc_forwards.
998-
route.paths[0].hops.last_mut().map(|h| h.cltv_expiry_delta += excess_final_cltv_delta_opt.unwrap() as u32);
999-
route.paths[0].blinded_tail.as_mut().map(|bt| bt.excess_final_cltv_expiry_delta = excess_final_cltv_delta_opt.unwrap() as u32);
1000-
route
996+
get_route(&nodes[0], &route_params).unwrap()
1001997
} else if check == ReceiveCheckFail::PaymentConstraints {
1002998
// Create a blinded path where the receiver's encrypted payload has an htlc_minimum_msat that is
1003999
// violated by `amt_msat`, and stick it in the route_params without changing the corresponding
@@ -1115,7 +1111,6 @@ fn do_multi_hop_receiver_fail(check: ReceiveCheckFail) {
11151111
check_added_monitors(&nodes[2], 1);
11161112
},
11171113
ReceiveCheckFail::ProcessPendingHTLCsCheck => {
1118-
assert_eq!(payment_event_1_2.msgs[0].cltv_expiry, nodes[0].best_block_info().1 + 1 + excess_final_cltv_delta_opt.unwrap() as u32 + TEST_FINAL_CLTV);
11191114
nodes[2].node.handle_update_add_htlc(nodes[1].node.get_our_node_id(), &payment_event_1_2.msgs[0]);
11201115
check_added_monitors(&nodes[2], 0);
11211116
do_commitment_signed_dance(&nodes[2], &nodes[1], &payment_event_1_2.commitment_msg, true, true);
@@ -1857,7 +1852,7 @@ fn test_combined_trampoline_onion_creation_vectors() {
18571852
short_channel_id: (572330 << 40) + (42 << 16) + 2821,
18581853
channel_features: ChannelFeatures::empty(),
18591854
fee_msat: 153_000,
1860-
cltv_expiry_delta: 0,
1855+
cltv_expiry_delta: 24 + 36,
18611856
maybe_announced_channel: false,
18621857
},
18631858
],
@@ -1952,7 +1947,7 @@ fn test_trampoline_inbound_payment_decoding() {
19521947
short_channel_id: (572330 << 40) + (42 << 16) + 2821,
19531948
channel_features: ChannelFeatures::empty(),
19541949
fee_msat: 150_153_000,
1955-
cltv_expiry_delta: 0,
1950+
cltv_expiry_delta: 24 + 36,
19561951
maybe_announced_channel: false,
19571952
},
19581953
],
@@ -2120,7 +2115,7 @@ fn test_trampoline_forward_payload_encoded_as_receive() {
21202115
blinded_path::utils::construct_blinded_hops(
21212116
&secp_ctx, path.into_iter(), &trampoline_session_priv,
21222117
)
2123-
};
2118+
};
21242119

21252120
let route = Route {
21262121
paths: vec![Path {
@@ -2143,7 +2138,7 @@ fn test_trampoline_forward_payload_encoded_as_receive() {
21432138
short_channel_id: bob_carol_scid,
21442139
channel_features: ChannelFeatures::empty(),
21452140
fee_msat: 0,
2146-
cltv_expiry_delta: 48,
2141+
cltv_expiry_delta: 24 + 39,
21472142
maybe_announced_channel: false,
21482143
}
21492144
],
@@ -2154,7 +2149,7 @@ fn test_trampoline_forward_payload_encoded_as_receive() {
21542149
pubkey: carol_node_id,
21552150
node_features: Features::empty(),
21562151
fee_msat: amt_msat,
2157-
cltv_expiry_delta: 24,
2152+
cltv_expiry_delta: 24 + 39,
21582153
},
21592154
],
21602155
hops: carol_blinded_hops,
@@ -2181,7 +2176,7 @@ fn test_trampoline_forward_payload_encoded_as_receive() {
21812176
});
21822177

21832178
let recipient_onion_fields = RecipientOnionFields::spontaneous_empty(amt_msat);
2184-
let (mut trampoline_payloads, outer_total_msat, outer_starting_htlc_offset) = onion_utils::build_trampoline_onion_payloads(&blinded_tail, &recipient_onion_fields, 32, &None).unwrap();
2179+
let (mut trampoline_payloads, outer_total_msat) = onion_utils::build_trampoline_onion_payloads(&blinded_tail, &recipient_onion_fields, 32, &None).unwrap();
21852180

21862181
// pop the last dummy hop
21872182
trampoline_payloads.pop();
@@ -2196,7 +2191,7 @@ fn test_trampoline_forward_payload_encoded_as_receive() {
21962191
).unwrap();
21972192

21982193
let recipient_onion_fields = RecipientOnionFields::spontaneous_empty(outer_total_msat);
2199-
let (outer_payloads, _, _) = onion_utils::test_build_onion_payloads(&route.paths[0], &recipient_onion_fields, outer_starting_htlc_offset, &None, None, Some(trampoline_packet)).unwrap();
2194+
let (outer_payloads, _, _) = onion_utils::test_build_onion_payloads(&route.paths[0], &recipient_onion_fields, 32, &None, None, Some(trampoline_packet)).unwrap();
22002195
let outer_onion_keys = onion_utils::construct_onion_keys(&secp_ctx, &route.clone().paths[0], &outer_session_priv);
22012196
let outer_packet = onion_utils::construct_onion_packet(
22022197
outer_payloads,
@@ -2309,7 +2304,7 @@ fn do_test_trampoline_single_hop_receive(success: bool) {
23092304
short_channel_id: bob_carol_scid,
23102305
channel_features: ChannelFeatures::empty(),
23112306
fee_msat: 0,
2312-
cltv_expiry_delta: 48,
2307+
cltv_expiry_delta: 104 + 39,
23132308
maybe_announced_channel: false,
23142309
}
23152310
],
@@ -2320,7 +2315,7 @@ fn do_test_trampoline_single_hop_receive(success: bool) {
23202315
pubkey: carol_node_id,
23212316
node_features: Features::empty(),
23222317
fee_msat: amt_msat,
2323-
cltv_expiry_delta: 104,
2318+
cltv_expiry_delta: 104 + 39,
23242319
},
23252320
],
23262321
hops: blinded_path.blinded_hops().to_vec(),
@@ -2428,8 +2423,8 @@ fn test_trampoline_blinded_receive() {
24282423
/// Creates a blinded tail where Carol receives via a blinded path.
24292424
fn create_blinded_tail(
24302425
secp_ctx: &Secp256k1<All>, override_random_bytes: [u8; 32], carol_node_id: PublicKey,
2431-
carol_auth_key: ReceiveAuthKey, trampoline_cltv_expiry_delta: u32, final_value_msat: u64,
2432-
payment_secret: PaymentSecret,
2426+
carol_auth_key: ReceiveAuthKey, trampoline_cltv_expiry_delta: u32,
2427+
excess_final_cltv_delta: u32, final_value_msat: u64, payment_secret: PaymentSecret,
24332428
) -> BlindedTail {
24342429
let outer_session_priv = SecretKey::from_slice(&override_random_bytes).unwrap();
24352430
let trampoline_session_priv = onion_utils::compute_trampoline_session_priv(&outer_session_priv);
@@ -2460,11 +2455,11 @@ fn create_blinded_tail(
24602455
pubkey: carol_node_id,
24612456
node_features: Features::empty(),
24622457
fee_msat: final_value_msat,
2463-
cltv_expiry_delta: trampoline_cltv_expiry_delta,
2458+
cltv_expiry_delta: trampoline_cltv_expiry_delta + excess_final_cltv_delta,
24642459
}],
24652460
hops: carol_blinded_hops,
24662461
blinding_point: carol_blinding_point,
2467-
excess_final_cltv_expiry_delta: 39,
2462+
excess_final_cltv_expiry_delta: excess_final_cltv_delta,
24682463
final_value_msat,
24692464
}
24702465
}
@@ -2473,8 +2468,9 @@ fn create_blinded_tail(
24732468
// payloads that send to unblinded receives and invalid payloads.
24742469
fn replacement_onion(
24752470
test_case: TrampolineTestCase, secp_ctx: &Secp256k1<All>, override_random_bytes: [u8; 32],
2476-
route: Route, original_amt_msat: u64, starting_htlc_offset: u32, original_trampoline_cltv: u32,
2477-
payment_hash: PaymentHash, payment_secret: PaymentSecret, blinded: bool,
2471+
route: Route, original_amt_msat: u64, starting_htlc_offset: u32, excess_final_cltv: u32,
2472+
original_trampoline_cltv: u32, payment_hash: PaymentHash, payment_secret: PaymentSecret,
2473+
blinded: bool,
24782474
) -> msgs::OnionPacket {
24792475
let outer_session_priv = SecretKey::from_slice(&override_random_bytes[..]).unwrap();
24802476
let trampoline_session_priv = onion_utils::compute_trampoline_session_priv(&outer_session_priv);
@@ -2485,8 +2481,8 @@ fn replacement_onion(
24852481
// Rebuild our trampoline packet from the original route. If we want to test Carol receiving
24862482
// as an unblinded trampoline hop, we switch out her inner trampoline onion with a direct
24872483
// receive payload because LDK doesn't support unblinded trampoline receives.
2488-
let (trampoline_packet, outer_total_msat, outer_starting_htlc_offset) = {
2489-
let (mut trampoline_payloads, outer_total_msat, outer_starting_htlc_offset) =
2484+
let (trampoline_packet, outer_total_msat) = {
2485+
let (mut trampoline_payloads, outer_total_msat) =
24902486
onion_utils::build_trampoline_onion_payloads(
24912487
&blinded_tail,
24922488
&recipient_onion_fields,
@@ -2502,7 +2498,9 @@ fn replacement_onion(
25022498
total_msat: original_amt_msat,
25032499
}),
25042500
sender_intended_htlc_amt_msat: original_amt_msat,
2505-
cltv_expiry_height: original_trampoline_cltv + starting_htlc_offset,
2501+
cltv_expiry_height: original_trampoline_cltv
2502+
+ starting_htlc_offset
2503+
+ excess_final_cltv,
25062504
}];
25072505
}
25082506

@@ -2520,7 +2518,7 @@ fn replacement_onion(
25202518
)
25212519
.unwrap();
25222520

2523-
(trampoline_packet, outer_total_msat, outer_starting_htlc_offset)
2521+
(trampoline_packet, outer_total_msat)
25242522
};
25252523

25262524
// Use a different session key to construct the replacement onion packet. Note that the
@@ -2529,7 +2527,7 @@ fn replacement_onion(
25292527
let (mut outer_payloads, _, _) = onion_utils::test_build_onion_payloads(
25302528
&route.paths[0],
25312529
&recipient_onion_fields,
2532-
outer_starting_htlc_offset,
2530+
starting_htlc_offset,
25332531
&None,
25342532
None,
25352533
Some(trampoline_packet),
@@ -2547,7 +2545,7 @@ fn replacement_onion(
25472545
..
25482546
} => {
25492547
*amt_to_forward = test_case.outer_onion_amt(original_amt_msat);
2550-
let outer_cltv = original_trampoline_cltv + starting_htlc_offset;
2548+
let outer_cltv = original_trampoline_cltv + starting_htlc_offset + excess_final_cltv;
25512549
*outgoing_cltv_value = test_case.outer_onion_cltv(outer_cltv);
25522550
},
25532551
_ => panic!("final payload is not trampoline entrypoint"),
@@ -2582,11 +2580,9 @@ fn do_test_trampoline_relay(blinded: bool, test_case: TrampolineTestCase) {
25822580
let alice_bob_chan = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0);
25832581
let bob_carol_chan = create_announced_chan_between_nodes_with_value(&nodes, 1, 2, 1_000_000, 0);
25842582

2583+
let starting_htlc_offset = (TOTAL_NODE_COUNT as u32) * CHAN_CONFIRM_DEPTH + 1;
25852584
for i in 0..TOTAL_NODE_COUNT {
2586-
connect_blocks(
2587-
&nodes[i],
2588-
(TOTAL_NODE_COUNT as u32) * CHAN_CONFIRM_DEPTH + 1 - nodes[i].best_block_info().1,
2589-
);
2585+
connect_blocks(&nodes[i], starting_htlc_offset - nodes[i].best_block_info().1);
25902586
}
25912587

25922588
let alice_node_id = nodes[0].node.get_our_node_id();
@@ -2597,8 +2593,11 @@ fn do_test_trampoline_relay(blinded: bool, test_case: TrampolineTestCase) {
25972593
let bob_carol_scid = get_scid_from_channel_id(&nodes[1], bob_carol_chan.2);
25982594

25992595
let original_amt_msat = 1000;
2600-
let original_trampoline_cltv = 72;
2601-
let starting_htlc_offset = 32;
2596+
// Note that for TrampolineTestCase::OuterCLTVLessThanTrampoline to work properly,
2597+
// (starting_htlc_offset + excess_final_cltv) / 2 < (starting_htlc_offset + excess_final_cltv + original_trampoline_cltv)
2598+
// otherwise dividing the CLTV value by 2 won't kick us under the outer trampoline CLTV.
2599+
let original_trampoline_cltv = 42;
2600+
let excess_final_cltv = 70;
26022601

26032602
let (payment_preimage, payment_hash, payment_secret) =
26042603
get_payment_preimage_hash(&nodes[2], Some(original_amt_msat), None);
@@ -2625,7 +2624,7 @@ fn do_test_trampoline_relay(blinded: bool, test_case: TrampolineTestCase) {
26252624
short_channel_id: bob_carol_scid,
26262625
channel_features: ChannelFeatures::empty(),
26272626
fee_msat: 0,
2628-
cltv_expiry_delta: 48,
2627+
cltv_expiry_delta: original_trampoline_cltv + excess_final_cltv,
26292628
maybe_announced_channel: false,
26302629
},
26312630
],
@@ -2638,6 +2637,7 @@ fn do_test_trampoline_relay(blinded: bool, test_case: TrampolineTestCase) {
26382637
carol_node_id,
26392638
nodes[2].keys_manager.get_receive_auth_key(),
26402639
original_trampoline_cltv,
2640+
excess_final_cltv,
26412641
original_amt_msat,
26422642
payment_secret,
26432643
)),
@@ -2680,6 +2680,7 @@ fn do_test_trampoline_relay(blinded: bool, test_case: TrampolineTestCase) {
26802680
original_amt_msat,
26812681
starting_htlc_offset,
26822682
original_trampoline_cltv,
2683+
excess_final_cltv,
26832684
payment_hash,
26842685
payment_secret,
26852686
blinded,
@@ -2696,8 +2697,9 @@ fn do_test_trampoline_relay(blinded: bool, test_case: TrampolineTestCase) {
26962697
);
26972698

26982699
let amt_bytes = test_case.outer_onion_amt(original_amt_msat).to_be_bytes();
2699-
let cltv_bytes =
2700-
test_case.outer_onion_cltv(original_trampoline_cltv + starting_htlc_offset).to_be_bytes();
2700+
let cltv_bytes = test_case
2701+
.outer_onion_cltv(original_trampoline_cltv + starting_htlc_offset + excess_final_cltv)
2702+
.to_be_bytes();
27012703
let payment_failure = test_case.payment_failed_conditions(&amt_bytes, &cltv_bytes).map(|p| {
27022704
if blinded {
27032705
PaymentFailedConditions::new()
@@ -2711,7 +2713,8 @@ fn do_test_trampoline_relay(blinded: bool, test_case: TrampolineTestCase) {
27112713
.without_claimable_event()
27122714
.expect_failure(HTLCHandlingFailureType::Receive { payment_hash })
27132715
} else {
2714-
args.with_payment_secret(payment_secret)
2716+
let htlc_cltv = starting_htlc_offset + original_trampoline_cltv + excess_final_cltv;
2717+
args.with_payment_secret(payment_secret).with_payment_claimable_cltv(htlc_cltv)
27152718
};
27162719

27172720
do_pass_along_path(args);
@@ -2797,7 +2800,7 @@ fn test_trampoline_forward_rejection() {
27972800
short_channel_id: bob_carol_scid,
27982801
channel_features: ChannelFeatures::empty(),
27992802
fee_msat: 0,
2800-
cltv_expiry_delta: 48,
2803+
cltv_expiry_delta: 24 + 24 + 39,
28012804
maybe_announced_channel: false,
28022805
}
28032806
],
@@ -2816,7 +2819,7 @@ fn test_trampoline_forward_rejection() {
28162819
pubkey: alice_node_id,
28172820
node_features: Features::empty(),
28182821
fee_msat: amt_msat,
2819-
cltv_expiry_delta: 24,
2822+
cltv_expiry_delta: 24 + 39,
28202823
},
28212824
],
28222825
hops: vec![BlindedHop{

lightning/src/ln/functional_test_utils.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
//! nodes for functional tests.
1212
1313
use crate::blinded_path::payment::DummyTlvs;
14-
use crate::chain::channelmonitor::ChannelMonitor;
14+
use crate::chain::channelmonitor::{ChannelMonitor, HTLC_FAIL_BACK_BUFFER};
1515
use crate::chain::transaction::OutPoint;
1616
use crate::chain::{BestBlock, ChannelMonitorUpdateStatus, Confirm, Listen, Watch};
1717
use crate::events::bump_transaction::sync::BumpTransactionEventHandlerSync;
@@ -3542,6 +3542,7 @@ pub struct PassAlongPathArgs<'a, 'b, 'c, 'd> {
35423542
pub custom_tlvs: Vec<(u64, Vec<u8>)>,
35433543
pub payment_metadata: Option<Vec<u8>>,
35443544
pub expected_failure: Option<HTLCHandlingFailureType>,
3545+
pub payment_claimable_cltv: Option<u32>,
35453546
}
35463547

35473548
impl<'a, 'b, 'c, 'd> PassAlongPathArgs<'a, 'b, 'c, 'd> {
@@ -3564,6 +3565,7 @@ impl<'a, 'b, 'c, 'd> PassAlongPathArgs<'a, 'b, 'c, 'd> {
35643565
custom_tlvs: Vec::new(),
35653566
payment_metadata: None,
35663567
expected_failure: None,
3568+
payment_claimable_cltv: None,
35673569
}
35683570
}
35693571
pub fn without_clearing_recipient_events(mut self) -> Self {
@@ -3604,6 +3606,10 @@ impl<'a, 'b, 'c, 'd> PassAlongPathArgs<'a, 'b, 'c, 'd> {
36043606
self.dummy_tlvs = dummy_tlvs.to_vec();
36053607
self
36063608
}
3609+
pub fn with_payment_claimable_cltv(mut self, cltv: u32) -> Self {
3610+
self.payment_claimable_cltv = Some(cltv);
3611+
self
3612+
}
36073613
}
36083614

36093615
pub fn do_pass_along_path<'a, 'b, 'c>(args: PassAlongPathArgs) -> Option<Event> {
@@ -3622,6 +3628,7 @@ pub fn do_pass_along_path<'a, 'b, 'c>(args: PassAlongPathArgs) -> Option<Event>
36223628
custom_tlvs,
36233629
payment_metadata,
36243630
expected_failure,
3631+
payment_claimable_cltv,
36253632
} = args;
36263633

36273634
let mut payment_event = SendEvent::from_event(ev);
@@ -3737,6 +3744,12 @@ pub fn do_pass_along_path<'a, 'b, 'c>(args: PassAlongPathArgs) -> Option<Event>
37373744
assert_eq!(*user_chan_id, Some(chan.user_channel_id));
37383745
}
37393746
assert!(claim_deadline.unwrap() > node.best_block_info().1);
3747+
if let Some(expected_cltv) = payment_claimable_cltv {
3748+
assert_eq!(
3749+
claim_deadline.unwrap(),
3750+
expected_cltv - HTLC_FAIL_BACK_BUFFER,
3751+
);
3752+
}
37403753
},
37413754
_ => panic!("Unexpected event"),
37423755
}

lightning/src/ln/htlc_reserve_unit_tests.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1429,9 +1429,10 @@ pub fn test_update_add_htlc_bolt2_sender_cltv_expiry_too_high() {
14291429

14301430
let _chan = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1000000, 0);
14311431

1432-
let payment_params = PaymentParameters::from_node_id(node_b_id, 0)
1432+
let mut payment_params = PaymentParameters::from_node_id(node_b_id, 0)
14331433
.with_bolt11_features(nodes[1].node.bolt11_invoice_features())
14341434
.unwrap();
1435+
payment_params.max_total_cltv_expiry_delta = 500000001;
14351436
let (mut route, our_payment_hash, _, our_payment_secret) =
14361437
get_route_and_payment_hash!(nodes[0], nodes[1], payment_params, 100000000);
14371438
route.paths[0].hops.last_mut().unwrap().cltv_expiry_delta = 500000001;

lightning/src/ln/onion_payment.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -438,7 +438,7 @@ pub(super) fn create_recv_pending_htlc_info(
438438
payment_data,
439439
payment_preimage,
440440
payment_metadata,
441-
incoming_cltv_expiry: onion_cltv_expiry,
441+
incoming_cltv_expiry: cltv_expiry,
442442
custom_tlvs,
443443
requires_blinded_error,
444444
has_recipient_created_payment_secret,
@@ -450,7 +450,7 @@ pub(super) fn create_recv_pending_htlc_info(
450450
payment_data: data,
451451
payment_metadata,
452452
payment_context,
453-
incoming_cltv_expiry: onion_cltv_expiry,
453+
incoming_cltv_expiry: cltv_expiry,
454454
phantom_shared_secret,
455455
trampoline_shared_secret,
456456
custom_tlvs,
@@ -842,7 +842,7 @@ mod tests {
842842
PendingHTLCRouting::ReceiveKeysend { payment_preimage, payment_data, incoming_cltv_expiry, .. } => {
843843
assert_eq!(payment_preimage, preimage);
844844
assert_eq!(peeled2.outgoing_amt_msat, recipient_amount);
845-
assert_eq!(incoming_cltv_expiry, peeled2.outgoing_cltv_value);
845+
assert_eq!(incoming_cltv_expiry, msg.cltv_expiry);
846846
let msgs::FinalOnionHopData{total_msat, payment_secret} = payment_data.unwrap();
847847
assert_eq!(total_msat, total_amt_msat);
848848
assert_eq!(payment_secret, pay_secret);

0 commit comments

Comments
 (0)