Fix trampoline onion encoding to match doc-declared CLTV rules
What changed, and why it matters
This commit fixes a mismatch between the documented behavior and the actual code for building trampoline payment onions in the Lightning Dev Kit. The documentation said that a route hop's time-lock delta already includes any trampoline hops, but the onion-building code was not doing that. The fix changes the code to match the documented (and simpler, more backwards-compatible) behavior. In practical terms, before the fix, trampoline payments could have been constructed with incorrect CLTV (time-lock) values, which could cause payment failures or, in edge cases, affect safety margins for time-locked refunds.
Review and merge the patch, then run the updated trampoline and blinded-path payment tests. Operators using trampoline payments should upgrade to ensure their outbound onions conform to the documented CLTV semantics and avoid payment failures or reduced time-lock safety margins.
Security signals we found
CLTV/time-lock mismatch between documentation and implementation
Trampoline onion payload construction bug
Potential payment failure or refund-safety margin reduction due to incorrect outgoing_cltv_value
Backwards-compatible API contract enforcement
No explicit vendor security disclosure or CVE referenced in commit
Evidence from the diff
The patch modifies build_trampoline_onion_payloads and build_onion_payloads_callback in lightning/src/ln/onion_utils.rs so that the outer onion’s CLTV accumulation includes the trampoline hops’ cltv_expiry_delta values, matching the documented API contract for RouteHop::cltv_expiry_delta and TrampolineHop::cltv_expiry_delta. Previously, the trampoline onion builder returned a separate outer_starting_htlc_offset that was used as the starting CLTV for the outer onion, effectively excluding trampoline CLTV deltas from the outer onion’s time-lock chain. The fix removes that extra return value and instead accumulates hop.cltv_expiry_delta().saturating_add(cur_cltv) consistently, including for the trampoline entry payload. Test vectors and helper functions across blinded_payment_tests.rs, functional_test_utils.rs, onion_route_tests.rs, and doc comments in router.rs are updated to reflect the new accumulation rules.
Changed components
lightning/src/ln/onion_utils.rslightning/src/ln/blinded_payment_tests.rslightning/src/ln/functional_test_utils.rslightning/src/ln/onion_route_tests.rslightning/src/routing/router.rsInspect captured patch +81 / −67
diff --git a/lightning/src/ln/blinded_payment_tests.rs b/lightning/src/ln/blinded_payment_tests.rs
index b945b89..e148ce2 100644
--- a/lightning/src/ln/blinded_payment_tests.rs
+++ b/lightning/src/ln/blinded_payment_tests.rs
@@ -1852,7 +1852,7 @@ fn test_combined_trampoline_onion_creation_vectors() {
short_channel_id: (572330 << 40) + (42 << 16) + 2821,
channel_features: ChannelFeatures::empty(),
fee_msat: 153_000,
- cltv_expiry_delta: 0,
+ cltv_expiry_delta: 24 + 36,
maybe_announced_channel: false,
},
],
@@ -1947,7 +1947,7 @@ fn test_trampoline_inbound_payment_decoding() {
short_channel_id: (572330 << 40) + (42 << 16) + 2821,
channel_features: ChannelFeatures::empty(),
fee_msat: 150_153_000,
- cltv_expiry_delta: 0,
+ cltv_expiry_delta: 24 + 36,
maybe_announced_channel: false,
},
],
@@ -2115,7 +2115,7 @@ fn test_trampoline_forward_payload_encoded_as_receive() {
blinded_path::utils::construct_blinded_hops(
&secp_ctx, path.into_iter(), &trampoline_session_priv,
)
- };
+ };
let route = Route {
paths: vec![Path {
@@ -2138,7 +2138,7 @@ fn test_trampoline_forward_payload_encoded_as_receive() {
short_channel_id: bob_carol_scid,
channel_features: ChannelFeatures::empty(),
fee_msat: 0,
- cltv_expiry_delta: 48,
+ cltv_expiry_delta: 24 + 39,
maybe_announced_channel: false,
}
],
@@ -2149,7 +2149,7 @@ fn test_trampoline_forward_payload_encoded_as_receive() {
pubkey: carol_node_id,
node_features: Features::empty(),
fee_msat: amt_msat,
- cltv_expiry_delta: 24,
+ cltv_expiry_delta: 24 + 39,
},
],
hops: carol_blinded_hops,
@@ -2176,7 +2176,7 @@ fn test_trampoline_forward_payload_encoded_as_receive() {
});
let recipient_onion_fields = RecipientOnionFields::spontaneous_empty(amt_msat);
- 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();
+ let (mut trampoline_payloads, outer_total_msat) = onion_utils::build_trampoline_onion_payloads(&blinded_tail, &recipient_onion_fields, 32, &None).unwrap();
// pop the last dummy hop
trampoline_payloads.pop();
@@ -2191,7 +2191,7 @@ fn test_trampoline_forward_payload_encoded_as_receive() {
).unwrap();
let recipient_onion_fields = RecipientOnionFields::spontaneous_empty(outer_total_msat);
- 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();
+ let (outer_payloads, _, _) = onion_utils::test_build_onion_payloads(&route.paths[0], &recipient_onion_fields, 32, &None, None, Some(trampoline_packet)).unwrap();
let outer_onion_keys = onion_utils::construct_onion_keys(&secp_ctx, &route.clone().paths[0], &outer_session_priv);
let outer_packet = onion_utils::construct_onion_packet(
outer_payloads,
@@ -2304,7 +2304,7 @@ fn do_test_trampoline_single_hop_receive(success: bool) {
short_channel_id: bob_carol_scid,
channel_features: ChannelFeatures::empty(),
fee_msat: 0,
- cltv_expiry_delta: 48,
+ cltv_expiry_delta: 104 + 39,
maybe_announced_channel: false,
}
],
@@ -2315,7 +2315,7 @@ fn do_test_trampoline_single_hop_receive(success: bool) {
pubkey: carol_node_id,
node_features: Features::empty(),
fee_msat: amt_msat,
- cltv_expiry_delta: 104,
+ cltv_expiry_delta: 104 + 39,
},
],
hops: blinded_path.blinded_hops().to_vec(),
@@ -2423,8 +2423,8 @@ fn test_trampoline_blinded_receive() {
/// Creates a blinded tail where Carol receives via a blinded path.
fn create_blinded_tail(
secp_ctx: &Secp256k1<All>, override_random_bytes: [u8; 32], carol_node_id: PublicKey,
- carol_auth_key: ReceiveAuthKey, trampoline_cltv_expiry_delta: u32, final_value_msat: u64,
- payment_secret: PaymentSecret,
+ carol_auth_key: ReceiveAuthKey, trampoline_cltv_expiry_delta: u32,
+ excess_final_cltv_delta: u32, final_value_msat: u64, payment_secret: PaymentSecret,
) -> BlindedTail {
let outer_session_priv = SecretKey::from_slice(&override_random_bytes).unwrap();
let trampoline_session_priv = onion_utils::compute_trampoline_session_priv(&outer_session_priv);
@@ -2455,11 +2455,11 @@ fn create_blinded_tail(
pubkey: carol_node_id,
node_features: Features::empty(),
fee_msat: final_value_msat,
- cltv_expiry_delta: trampoline_cltv_expiry_delta,
+ cltv_expiry_delta: trampoline_cltv_expiry_delta + excess_final_cltv_delta,
}],
hops: carol_blinded_hops,
blinding_point: carol_blinding_point,
- excess_final_cltv_expiry_delta: 39,
+ excess_final_cltv_expiry_delta: excess_final_cltv_delta,
final_value_msat,
}
}
@@ -2468,8 +2468,9 @@ fn create_blinded_tail(
// payloads that send to unblinded receives and invalid payloads.
fn replacement_onion(
test_case: TrampolineTestCase, secp_ctx: &Secp256k1<All>, override_random_bytes: [u8; 32],
- route: Route, original_amt_msat: u64, starting_htlc_offset: u32, original_trampoline_cltv: u32,
- payment_hash: PaymentHash, payment_secret: PaymentSecret, blinded: bool,
+ route: Route, original_amt_msat: u64, starting_htlc_offset: u32, excess_final_cltv: u32,
+ original_trampoline_cltv: u32, payment_hash: PaymentHash, payment_secret: PaymentSecret,
+ blinded: bool,
) -> msgs::OnionPacket {
let outer_session_priv = SecretKey::from_slice(&override_random_bytes[..]).unwrap();
let trampoline_session_priv = onion_utils::compute_trampoline_session_priv(&outer_session_priv);
@@ -2480,8 +2481,8 @@ fn replacement_onion(
// Rebuild our trampoline packet from the original route. If we want to test Carol receiving
// as an unblinded trampoline hop, we switch out her inner trampoline onion with a direct
// receive payload because LDK doesn't support unblinded trampoline receives.
- let (trampoline_packet, outer_total_msat, outer_starting_htlc_offset) = {
- let (mut trampoline_payloads, outer_total_msat, outer_starting_htlc_offset) =
+ let (trampoline_packet, outer_total_msat) = {
+ let (mut trampoline_payloads, outer_total_msat) =
onion_utils::build_trampoline_onion_payloads(
&blinded_tail,
&recipient_onion_fields,
@@ -2497,7 +2498,9 @@ fn replacement_onion(
total_msat: original_amt_msat,
}),
sender_intended_htlc_amt_msat: original_amt_msat,
- cltv_expiry_height: original_trampoline_cltv + starting_htlc_offset,
+ cltv_expiry_height: original_trampoline_cltv
+ + starting_htlc_offset
+ + excess_final_cltv,
}];
}
@@ -2515,7 +2518,7 @@ fn replacement_onion(
)
.unwrap();
- (trampoline_packet, outer_total_msat, outer_starting_htlc_offset)
+ (trampoline_packet, outer_total_msat)
};
// Use a different session key to construct the replacement onion packet. Note that the
@@ -2524,7 +2527,7 @@ fn replacement_onion(
let (mut outer_payloads, _, _) = onion_utils::test_build_onion_payloads(
&route.paths[0],
&recipient_onion_fields,
- outer_starting_htlc_offset,
+ starting_htlc_offset,
&None,
None,
Some(trampoline_packet),
@@ -2542,7 +2545,7 @@ fn replacement_onion(
..
} => {
*amt_to_forward = test_case.outer_onion_amt(original_amt_msat);
- let outer_cltv = original_trampoline_cltv + starting_htlc_offset;
+ let outer_cltv = original_trampoline_cltv + starting_htlc_offset + excess_final_cltv;
*outgoing_cltv_value = test_case.outer_onion_cltv(outer_cltv);
},
_ => panic!("final payload is not trampoline entrypoint"),
@@ -2577,11 +2580,9 @@ fn do_test_trampoline_relay(blinded: bool, test_case: TrampolineTestCase) {
let alice_bob_chan = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0);
let bob_carol_chan = create_announced_chan_between_nodes_with_value(&nodes, 1, 2, 1_000_000, 0);
+ let starting_htlc_offset = (TOTAL_NODE_COUNT as u32) * CHAN_CONFIRM_DEPTH + 1;
for i in 0..TOTAL_NODE_COUNT {
- connect_blocks(
- &nodes[i],
- (TOTAL_NODE_COUNT as u32) * CHAN_CONFIRM_DEPTH + 1 - nodes[i].best_block_info().1,
- );
+ connect_blocks(&nodes[i], starting_htlc_offset - nodes[i].best_block_info().1);
}
let alice_node_id = nodes[0].node.get_our_node_id();
@@ -2592,8 +2593,11 @@ fn do_test_trampoline_relay(blinded: bool, test_case: TrampolineTestCase) {
let bob_carol_scid = get_scid_from_channel_id(&nodes[1], bob_carol_chan.2);
let original_amt_msat = 1000;
- let original_trampoline_cltv = 72;
- let starting_htlc_offset = 32;
+ // Note that for TrampolineTestCase::OuterCLTVLessThanTrampoline to work properly,
+ // (starting_htlc_offset + excess_final_cltv) / 2 < (starting_htlc_offset + excess_final_cltv + original_trampoline_cltv)
+ // otherwise dividing the CLTV value by 2 won't kick us under the outer trampoline CLTV.
+ let original_trampoline_cltv = 42;
+ let excess_final_cltv = 70;
let (payment_preimage, payment_hash, payment_secret) =
get_payment_preimage_hash(&nodes[2], Some(original_amt_msat), None);
@@ -2620,7 +2624,7 @@ fn do_test_trampoline_relay(blinded: bool, test_case: TrampolineTestCase) {
short_channel_id: bob_carol_scid,
channel_features: ChannelFeatures::empty(),
fee_msat: 0,
- cltv_expiry_delta: 48,
+ cltv_expiry_delta: original_trampoline_cltv + excess_final_cltv,
maybe_announced_channel: false,
},
],
@@ -2633,6 +2637,7 @@ fn do_test_trampoline_relay(blinded: bool, test_case: TrampolineTestCase) {
carol_node_id,
nodes[2].keys_manager.get_receive_auth_key(),
original_trampoline_cltv,
+ excess_final_cltv,
original_amt_msat,
payment_secret,
)),
@@ -2675,6 +2680,7 @@ fn do_test_trampoline_relay(blinded: bool, test_case: TrampolineTestCase) {
original_amt_msat,
starting_htlc_offset,
original_trampoline_cltv,
+ excess_final_cltv,
payment_hash,
payment_secret,
blinded,
@@ -2691,8 +2697,9 @@ fn do_test_trampoline_relay(blinded: bool, test_case: TrampolineTestCase) {
);
let amt_bytes = test_case.outer_onion_amt(original_amt_msat).to_be_bytes();
- let cltv_bytes =
- test_case.outer_onion_cltv(original_trampoline_cltv + starting_htlc_offset).to_be_bytes();
+ let cltv_bytes = test_case
+ .outer_onion_cltv(original_trampoline_cltv + starting_htlc_offset + excess_final_cltv)
+ .to_be_bytes();
let payment_failure = test_case.payment_failed_conditions(&amt_bytes, &cltv_bytes).map(|p| {
if blinded {
PaymentFailedConditions::new()
@@ -2706,7 +2713,8 @@ fn do_test_trampoline_relay(blinded: bool, test_case: TrampolineTestCase) {
.without_claimable_event()
.expect_failure(HTLCHandlingFailureType::Receive { payment_hash })
} else {
- args.with_payment_secret(payment_secret)
+ let htlc_cltv = starting_htlc_offset + original_trampoline_cltv + excess_final_cltv;
+ args.with_payment_secret(payment_secret).with_payment_claimable_cltv(htlc_cltv)
};
do_pass_along_path(args);
@@ -2792,7 +2800,7 @@ fn test_trampoline_forward_rejection() {
short_channel_id: bob_carol_scid,
channel_features: ChannelFeatures::empty(),
fee_msat: 0,
- cltv_expiry_delta: 48,
+ cltv_expiry_delta: 24 + 24 + 39,
maybe_announced_channel: false,
}
],
@@ -2811,7 +2819,7 @@ fn test_trampoline_forward_rejection() {
pubkey: alice_node_id,
node_features: Features::empty(),
fee_msat: amt_msat,
- cltv_expiry_delta: 24,
+ cltv_expiry_delta: 24 + 39,
},
],
hops: vec![BlindedHop{
diff --git a/lightning/src/ln/functional_test_utils.rs b/lightning/src/ln/functional_test_utils.rs
index 16616e5..680a0d9 100644
--- a/lightning/src/ln/functional_test_utils.rs
+++ b/lightning/src/ln/functional_test_utils.rs
@@ -11,7 +11,7 @@
//! nodes for functional tests.
use crate::blinded_path::payment::DummyTlvs;
-use crate::chain::channelmonitor::ChannelMonitor;
+use crate::chain::channelmonitor::{ChannelMonitor, HTLC_FAIL_BACK_BUFFER};
use crate::chain::transaction::OutPoint;
use crate::chain::{BestBlock, ChannelMonitorUpdateStatus, Confirm, Listen, Watch};
use crate::events::bump_transaction::sync::BumpTransactionEventHandlerSync;
@@ -3490,6 +3490,7 @@ pub struct PassAlongPathArgs<'a, 'b, 'c, 'd> {
pub custom_tlvs: Vec<(u64, Vec<u8>)>,
pub payment_metadata: Option<Vec<u8>>,
pub expected_failure: Option<HTLCHandlingFailureType>,
+ pub payment_claimable_cltv: Option<u32>,
}
impl<'a, 'b, 'c, 'd> PassAlongPathArgs<'a, 'b, 'c, 'd> {
@@ -3512,6 +3513,7 @@ impl<'a, 'b, 'c, 'd> PassAlongPathArgs<'a, 'b, 'c, 'd> {
custom_tlvs: Vec::new(),
payment_metadata: None,
expected_failure: None,
+ payment_claimable_cltv: None,
}
}
pub fn without_clearing_recipient_events(mut self) -> Self {
@@ -3552,6 +3554,10 @@ impl<'a, 'b, 'c, 'd> PassAlongPathArgs<'a, 'b, 'c, 'd> {
self.dummy_tlvs = dummy_tlvs.to_vec();
self
}
+ pub fn with_payment_claimable_cltv(mut self, cltv: u32) -> Self {
+ self.payment_claimable_cltv = Some(cltv);
+ self
+ }
}
pub fn do_pass_along_path<'a, 'b, 'c>(args: PassAlongPathArgs) -> Option<Event> {
@@ -3570,6 +3576,7 @@ pub fn do_pass_along_path<'a, 'b, 'c>(args: PassAlongPathArgs) -> Option<Event>
custom_tlvs,
payment_metadata,
expected_failure,
+ payment_claimable_cltv,
} = args;
let mut payment_event = SendEvent::from_event(ev);
@@ -3685,6 +3692,12 @@ pub fn do_pass_along_path<'a, 'b, 'c>(args: PassAlongPathArgs) -> Option<Event>
assert_eq!(*user_chan_id, Some(chan.user_channel_id));
}
assert!(claim_deadline.unwrap() > node.best_block_info().1);
+ if let Some(expected_cltv) = payment_claimable_cltv {
+ assert_eq!(
+ claim_deadline.unwrap(),
+ expected_cltv - HTLC_FAIL_BACK_BUFFER,
+ );
+ }
},
_ => panic!("Unexpected event"),
}
diff --git a/lightning/src/ln/onion_route_tests.rs b/lightning/src/ln/onion_route_tests.rs
index ceb9300..019d8fa 100644
--- a/lightning/src/ln/onion_route_tests.rs
+++ b/lightning/src/ln/onion_route_tests.rs
@@ -1918,7 +1918,7 @@ fn test_trampoline_onion_payload_assembly_values() {
short_channel_id: (572330 << 40) + (42 << 16) + 2821,
channel_features: ChannelFeatures::empty(),
fee_msat: 153_000,
- cltv_expiry_delta: 0,
+ cltv_expiry_delta: 36 + 24, // Last hop should include the CLTV of the trampoline hops
maybe_announced_channel: false,
},
],
@@ -1974,17 +1974,15 @@ fn test_trampoline_onion_payload_assembly_values() {
SecretKey::from_slice(&<Vec<u8>>::from_hex(SECRET_HEX).unwrap()).unwrap().secret_bytes(),
);
let recipient_onion_fields = RecipientOnionFields::secret_only(payment_secret, amt_msat);
- let (trampoline_payloads, outer_total_msat, outer_starting_htlc_offset) =
- onion_utils::build_trampoline_onion_payloads(
- &path.blinded_tail.as_ref().unwrap(),
- &recipient_onion_fields,
- cur_height,
- &None,
- )
- .unwrap();
+ let (trampoline_payloads, outer_total_msat) = onion_utils::build_trampoline_onion_payloads(
+ &path.blinded_tail.as_ref().unwrap(),
+ &recipient_onion_fields,
+ cur_height,
+ &None,
+ )
+ .unwrap();
assert_eq!(trampoline_payloads.len(), 3);
assert_eq!(outer_total_msat, 150_153_000);
- assert_eq!(outer_starting_htlc_offset, 800_060);
let trampoline_carol_payload = &trampoline_payloads[0];
let trampoline_dave_payload = &trampoline_payloads[1];
@@ -2042,7 +2040,7 @@ fn test_trampoline_onion_payload_assembly_values() {
let (outer_payloads, total_msat, total_htlc_offset) = test_build_onion_payloads(
&path,
&recipient_onion_fields,
- outer_starting_htlc_offset,
+ cur_height,
&None,
None,
Some(trampoline_packet),
@@ -2067,7 +2065,7 @@ fn test_trampoline_onion_payload_assembly_values() {
outer_bob_payload
{
assert_eq!(amt_to_forward, &150_153_000);
- assert_eq!(outgoing_cltv_value, &800_084);
+ assert_eq!(outgoing_cltv_value, &800_060);
} else {
panic!("Bob payload must be Forward");
}
diff --git a/lightning/src/ln/onion_utils.rs b/lightning/src/ln/onion_utils.rs
index a95012d..5c00368 100644
--- a/lightning/src/ln/onion_utils.rs
+++ b/lightning/src/ln/onion_utils.rs
@@ -416,7 +416,7 @@ pub(super) fn construct_trampoline_onion_keys<T: secp256k1::Signing>(
pub(super) fn build_trampoline_onion_payloads<'a>(
blinded_tail: &'a BlindedTail, recipient_onion: &'a RecipientOnionFields,
starting_htlc_offset: u32, keysend_preimage: &Option<PaymentPreimage>,
-) -> Result<(Vec<msgs::OutboundTrampolinePayload<'a>>, u64, u32), APIError> {
+) -> Result<(Vec<msgs::OutboundTrampolinePayload<'a>>, u64), APIError> {
let mut res: Vec<msgs::OutboundTrampolinePayload> =
Vec::with_capacity(blinded_tail.trampoline_hops.len() + blinded_tail.hops.len());
let blinded_tail_with_hop_iter = BlindedTailDetails::DirectEntry {
@@ -426,7 +426,7 @@ pub(super) fn build_trampoline_onion_payloads<'a>(
excess_final_cltv_expiry_delta: blinded_tail.excess_final_cltv_expiry_delta,
};
- let (value_msat, cltv) = build_onion_payloads_callback(
+ let (value_msat, _) = build_onion_payloads_callback(
blinded_tail.trampoline_hops.iter(),
Some(blinded_tail_with_hop_iter),
recipient_onion,
@@ -438,7 +438,7 @@ pub(super) fn build_trampoline_onion_payloads<'a>(
PayloadCallbackAction::PushFront => res.insert(0, payload),
},
)?;
- Ok((res, value_msat, cltv))
+ Ok((res, value_msat))
}
/// returns the hop data, as well as the first-hop value_msat and CLTV value we should send.
@@ -539,11 +539,7 @@ where
// exactly as it should be (and the next hop isn't trying to probe to find out if we're
// the intended recipient).
let value_msat = if cur_value_msat == 0 { hop.fee_msat() } else { cur_value_msat };
- let cltv = if cur_cltv == starting_htlc_offset {
- hop.cltv_expiry_delta().saturating_add(starting_htlc_offset)
- } else {
- cur_cltv
- };
+ let cltv = hop.cltv_expiry_delta().saturating_add(cur_cltv);
if idx == 0 {
match blinded_tail.take() {
Some(BlindedTailDetails::DirectEntry {
@@ -591,7 +587,7 @@ where
PayloadCallbackAction::PushBack,
OP::new_trampoline_entry(
final_value_msat + hop.fee_msat(),
- cur_cltv,
+ cltv,
&recipient_onion,
trampoline_packet,
)?,
@@ -610,7 +606,7 @@ where
err: "Next hop ID must be known for non-final hops".to_string(),
})?,
value_msat,
- cltv,
+ cur_cltv,
);
callback(PayloadCallbackAction::PushFront, payload);
}
@@ -2638,8 +2634,6 @@ pub(crate) fn create_payment_onion_internal<T: secp256k1::Signing>(
prng_seed: [u8; 32], trampoline_session_priv_override: Option<SecretKey>,
trampoline_prng_seed_override: Option<[u8; 32]>,
) -> Result<(msgs::OnionPacket, u64, u32), APIError> {
- let mut outer_starting_htlc_offset = cur_block_height;
-
// If we're paying to a recipient through a trampoline, we use the `payment_secret` provided in
// `recipient_onion` as the MPP identifier for the trampoline entry point, allowing it to
// detect when when it has received all the MPP parts.
@@ -2661,13 +2655,12 @@ pub(crate) fn create_payment_onion_internal<T: secp256k1::Signing>(
if !blinded_tail.trampoline_hops.is_empty() {
let trampoline_payloads;
let outer_total_msat;
- (trampoline_payloads, outer_total_msat, outer_starting_htlc_offset) =
- build_trampoline_onion_payloads(
- &blinded_tail,
- recipient_onion,
- cur_block_height,
- keysend_preimage,
- )?;
+ (trampoline_payloads, outer_total_msat) = build_trampoline_onion_payloads(
+ &blinded_tail,
+ recipient_onion,
+ cur_block_height,
+ keysend_preimage,
+ )?;
trampoline_outer_onion.total_mpp_amount_msat = outer_total_msat;
let trampoline_session_priv = trampoline_session_priv_override
@@ -2698,7 +2691,7 @@ pub(crate) fn create_payment_onion_internal<T: secp256k1::Signing>(
let (onion_payloads, htlc_msat, htlc_cltv) = build_onion_payloads(
&path,
outer_onion,
- outer_starting_htlc_offset,
+ cur_block_height,
keysend_preimage,
invoice_request,
trampoline_packet_option,
diff --git a/lightning/src/routing/router.rs b/lightning/src/routing/router.rs
index 75c6a05..ee08f9e 100644
--- a/lightning/src/routing/router.rs
+++ b/lightning/src/routing/router.rs
@@ -512,6 +512,7 @@ pub struct RouteHop {
/// to reach this node.
pub channel_features: ChannelFeatures,
/// The fee taken on this hop (for paying for the use of the *next* channel in the path).
+ ///
/// If this is the last hop in [`Path::hops`]:
/// * if we're sending to a [`BlindedPaymentPath`], this is the fee paid for use of the entire
/// blinded path (including any Trampoline hops)
@@ -557,8 +558,9 @@ pub struct TrampolineHop {
/// the entire blinded path.
pub fee_msat: u64,
/// The CLTV delta added for this hop.
+ ///
/// If this is the last Trampoline hop within [`BlindedTail`], this is the CLTV delta for the entire
- /// blinded path.
+ /// blinded path (including the [`BlindedTail::excess_final_cltv_expiry_delta`]).
pub cltv_expiry_delta: u32,
}
Why this scored 59/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.