Skip to content

Commit 9d8a82b

Browse files
committed
ln: add trampoline mpp accumulation with rejection on completion
Add our MPP accumulation logic for trampoline payments, but reject them when they fully arrive. This allows us to test parts of our trampoline flow without fully enabling it. This commit keeps the same committed_to_claimable debug_assert behavior as MPP claims, asserting that we do not fail our check_claimable_incoming_htlc merge for the first HTLC that we add to a set. This assert could also be hit if the intended amount exceeds `MAX_VALUE_MSAT`, but we can't hit this in practice.
1 parent 439c406 commit 9d8a82b

2 files changed

Lines changed: 235 additions & 5 deletions

File tree

lightning/src/ln/channelmanager.rs

Lines changed: 214 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,9 @@ use crate::ln::outbound_payment;
8989
#[cfg(any(test, feature = "_externalize_tests"))]
9090
use crate::ln::outbound_payment::PaymentSendFailure;
9191
use crate::ln::outbound_payment::{
92-
Bolt11PaymentError, Bolt12PaymentError, OutboundPayments, PendingOutboundPayment,
93-
ProbeSendFailure, RecipientCustomTlvs, RecipientOnionFields, Retry, RetryableInvoiceRequest,
94-
RetryableSendFailure, SendAlongPathArgs, StaleExpiration,
92+
Bolt11PaymentError, Bolt12PaymentError, NextTrampolineHopInfo, OutboundPayments,
93+
PendingOutboundPayment, ProbeSendFailure, RecipientCustomTlvs, RecipientOnionFields, Retry,
94+
RetryableInvoiceRequest, RetryableSendFailure, SendAlongPathArgs, StaleExpiration,
9595
};
9696
use crate::ln::types::ChannelId;
9797
use crate::offers::async_receive_offer_cache::AsyncReceiveOfferCache;
@@ -8470,6 +8470,130 @@ impl<
84708470
}
84718471
}
84728472

8473+
// Handles the addition of a HTLC associated with a trampoline forward that we need to accumulate
8474+
// on the incoming link before forwarding onwards. If the HTLC is failed, it returns the source
8475+
// and error that should be used to fail the HTLC(s) back.
8476+
fn handle_trampoline_htlc(
8477+
&self, mpp_part: MppPart, onion_fields: RecipientOnionFields, payment_hash: PaymentHash,
8478+
next_hop_info: NextTrampolineHopInfo, _next_node_id: PublicKey,
8479+
) -> Result<(), (HTLCSource, HTLCFailReason)> {
8480+
let mut trampoline_payments = self.awaiting_trampoline_forwards.lock().unwrap();
8481+
8482+
let mut committed_to_claimable = false;
8483+
let trampoline_payment = trampoline_payments.entry(payment_hash).or_insert_with(|| {
8484+
committed_to_claimable = true;
8485+
TrampolinePayment { htlcs: Vec::new(), onion_fields: onion_fields.clone() }
8486+
});
8487+
8488+
// If MPP hasn't fully arrived yet, return early (saving indentation below).
8489+
let prev_hop = mpp_part.prev_hop.clone();
8490+
match self.check_incoming_mpp_part(
8491+
&mut trampoline_payment.htlcs,
8492+
&mut trampoline_payment.onion_fields,
8493+
mpp_part,
8494+
onion_fields,
8495+
payment_hash,
8496+
) {
8497+
Ok(false) => return Ok(()),
8498+
Err(()) => {
8499+
if committed_to_claimable {
8500+
// If this was the first HTLC for this payment hash and check failed
8501+
// (eg, total_intended_recvd_value >= MAX_VALUE_MSAT), clean up the
8502+
// empty entry we just inserted.
8503+
trampoline_payments.remove(&payment_hash);
8504+
}
8505+
return Err((
8506+
// When we couldn't add a new HTLC, we just fail back our last received htlc,
8507+
// allowing others to wait for more MPP parts to arrive.
8508+
HTLCSource::TrampolineForward {
8509+
previous_hop_data: vec![prev_hop],
8510+
outbound_payment: None,
8511+
},
8512+
HTLCFailReason::reason(
8513+
LocalHTLCFailureReason::InvalidTrampolineForward,
8514+
vec![],
8515+
),
8516+
));
8517+
},
8518+
Ok(true) => {},
8519+
};
8520+
8521+
let incoming_amt_msat: u64 = trampoline_payment.htlcs.iter().map(|h| h.value).sum();
8522+
let incoming_cltv_expiry =
8523+
trampoline_payment.htlcs.iter().map(|h| h.cltv_expiry).min().unwrap();
8524+
8525+
let (forwarding_fee_proportional_millionths, forwarding_fee_base_msat, cltv_delta) = {
8526+
let config = self.config.read().unwrap();
8527+
(
8528+
config.channel_config.forwarding_fee_proportional_millionths,
8529+
config.channel_config.forwarding_fee_base_msat,
8530+
config.channel_config.cltv_expiry_delta as u32,
8531+
)
8532+
};
8533+
8534+
let proportional_fee = (forwarding_fee_proportional_millionths as u128
8535+
* next_hop_info.amount_msat as u128
8536+
/ 1_000_000) as u64;
8537+
let our_forwarding_fee_msat = proportional_fee + forwarding_fee_base_msat as u64;
8538+
8539+
let trampoline_source = || -> HTLCSource {
8540+
HTLCSource::TrampolineForward {
8541+
previous_hop_data: trampoline_payment
8542+
.htlcs
8543+
.iter()
8544+
.map(|htlc| htlc.prev_hop.clone())
8545+
.collect(),
8546+
outbound_payment: None,
8547+
}
8548+
};
8549+
let trampoline_failure = || -> HTLCFailReason {
8550+
let mut err_data = Vec::with_capacity(10);
8551+
err_data.extend_from_slice(&forwarding_fee_base_msat.to_be_bytes());
8552+
err_data.extend_from_slice(&forwarding_fee_proportional_millionths.to_be_bytes());
8553+
err_data.extend_from_slice(&(cltv_delta as u16).to_be_bytes());
8554+
HTLCFailReason::reason(
8555+
LocalHTLCFailureReason::TrampolineFeeOrExpiryInsufficient,
8556+
err_data,
8557+
)
8558+
};
8559+
8560+
let _max_total_routing_fee_msat = match incoming_amt_msat
8561+
.checked_sub(our_forwarding_fee_msat + next_hop_info.amount_msat)
8562+
{
8563+
Some(amount) => amount,
8564+
None => {
8565+
return Err((trampoline_source(), trampoline_failure()));
8566+
},
8567+
};
8568+
8569+
let _max_total_cltv_expiry_delta =
8570+
match incoming_cltv_expiry.checked_sub(next_hop_info.cltv_expiry_height + cltv_delta) {
8571+
Some(cltv_delta) => cltv_delta,
8572+
None => {
8573+
return Err((trampoline_source(), trampoline_failure()));
8574+
},
8575+
};
8576+
8577+
log_debug!(
8578+
self.logger,
8579+
"Rejecting trampoline forward because we do not fully support forwarding yet.",
8580+
);
8581+
8582+
let source = trampoline_source();
8583+
if trampoline_payments.remove(&payment_hash).is_none() {
8584+
log_error!(
8585+
&self.logger,
8586+
"Dispatched trampoline payment: {} was not present in awaiting inbound",
8587+
payment_hash
8588+
);
8589+
}
8590+
8591+
Err((
8592+
source,
8593+
HTLCFailReason::reason(LocalHTLCFailureReason::TemporaryTrampolineFailure, vec![]),
8594+
))
8595+
}
8596+
84738597
fn process_receive_htlcs(
84748598
&self, pending_forwards: &mut Vec<HTLCForwardInfo>,
84758599
new_events: &mut VecDeque<(Event, Option<EventCompletionAction>)>,
@@ -8504,6 +8628,7 @@ impl<
85048628
has_recipient_created_payment_secret,
85058629
invoice_request_opt,
85068630
trampoline_shared_secret,
8631+
trampoline_info,
85078632
) = match routing {
85088633
PendingHTLCRouting::Receive {
85098634
payment_data,
@@ -8532,6 +8657,7 @@ impl<
85328657
true,
85338658
None,
85348659
trampoline_shared_secret,
8660+
None,
85358661
)
85368662
},
85378663
PendingHTLCRouting::ReceiveKeysend {
@@ -8566,13 +8692,98 @@ impl<
85668692
has_recipient_created_payment_secret,
85678693
invoice_request,
85688694
None,
8695+
None,
8696+
)
8697+
},
8698+
PendingHTLCRouting::TrampolineForward {
8699+
trampoline_shared_secret: incoming_trampoline_shared_secret,
8700+
onion_packet,
8701+
node_id: next_trampoline,
8702+
blinded,
8703+
incoming_cltv_expiry,
8704+
incoming_multipath_data,
8705+
next_trampoline_amt_msat,
8706+
next_trampoline_cltv_expiry,
8707+
} => {
8708+
// Trampoline forwards only *need* to have MPP data if they're
8709+
// multi-part.
8710+
let onion_fields = match incoming_multipath_data {
8711+
Some(ref final_mpp) => RecipientOnionFields::secret_only(
8712+
final_mpp.payment_secret,
8713+
final_mpp.total_msat,
8714+
),
8715+
None => RecipientOnionFields::spontaneous_empty(outgoing_amt_msat),
8716+
};
8717+
8718+
let next_hop_info = NextTrampolineHopInfo {
8719+
onion_packet,
8720+
blinding_point: blinded.and_then(|b| {
8721+
b.next_blinding_override.or_else(|| {
8722+
let encrypted_tlvs_ss = self
8723+
.node_signer
8724+
.ecdh(Recipient::Node, &b.inbound_blinding_point, None)
8725+
.unwrap()
8726+
.secret_bytes();
8727+
onion_utils::next_hop_pubkey(
8728+
&self.secp_ctx,
8729+
b.inbound_blinding_point,
8730+
&encrypted_tlvs_ss,
8731+
)
8732+
.ok()
8733+
})
8734+
}),
8735+
amount_msat: next_trampoline_amt_msat,
8736+
cltv_expiry_height: next_trampoline_cltv_expiry,
8737+
};
8738+
(
8739+
incoming_cltv_expiry,
8740+
// Unused for trampoline forwards; MppPart is constructed
8741+
// directly below.
8742+
OnionPayload::Invoice { _legacy_hop_data: None },
8743+
incoming_multipath_data,
8744+
None,
8745+
None,
8746+
onion_fields,
8747+
false,
8748+
None,
8749+
Some(incoming_trampoline_shared_secret),
8750+
Some((next_hop_info, next_trampoline)),
85698751
)
85708752
},
85718753
_ => {
85728754
panic!("short_channel_id == 0 should imply any pending_forward entries are of type Receive");
85738755
},
85748756
};
85758757
let htlc_value = incoming_amt_msat.unwrap_or(outgoing_amt_msat);
8758+
// For trampoline forwards, construct MppPart directly and handle separately
8759+
// from claimable HTLCs.
8760+
if let Some((next_hop_info, next_trampoline)) = trampoline_info {
8761+
let mpp_part = MppPart {
8762+
prev_hop,
8763+
cltv_expiry,
8764+
value: htlc_value,
8765+
sender_intended_value: outgoing_amt_msat,
8766+
timer_ticks: 0,
8767+
total_value_received: None,
8768+
};
8769+
if let Err((htlc_source, failure_reason)) = self.handle_trampoline_htlc(
8770+
mpp_part,
8771+
onion_fields,
8772+
payment_hash,
8773+
next_hop_info,
8774+
next_trampoline,
8775+
) {
8776+
failed_forwards.push((
8777+
htlc_source,
8778+
payment_hash,
8779+
failure_reason,
8780+
HTLCHandlingFailureType::TrampolineForward {},
8781+
));
8782+
}
8783+
continue 'next_forwardable_htlc;
8784+
}
8785+
8786+
// If we don't have a trampoline forward, we're dealing with a MPP receive.
85768787
let htlc_source = HTLCSource::PreviousHopData(HTLCPreviousHopData {
85778788
prev_outbound_scid_alias: prev_hop.prev_outbound_scid_alias,
85788789
user_channel_id: prev_hop.user_channel_id,

lightning/src/ln/outbound_payment.rs

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
1212
use bitcoin::hashes::sha256::Hash as Sha256;
1313
use bitcoin::hashes::Hash;
14-
use bitcoin::secp256k1::{self, Secp256k1, SecretKey};
14+
use bitcoin::secp256k1::{self, PublicKey, Secp256k1, SecretKey};
1515
use lightning_invoice::Bolt11Invoice;
1616

1717
use crate::blinded_path::{IntroductionNode, NodeIdLookUp};
@@ -21,7 +21,7 @@ use crate::ln::channelmanager::{
2121
EventCompletionAction, HTLCSource, OptionalBolt11PaymentParams, PaymentCompleteUpdate,
2222
PaymentId,
2323
};
24-
use crate::ln::msgs::DecodeError;
24+
use crate::ln::msgs::{DecodeError, TrampolineOnionPacket};
2525
use crate::ln::onion_utils;
2626
use crate::ln::onion_utils::{DecodedOnionFailure, HTLCFailReason};
2727
use crate::offers::invoice::{Bolt12Invoice, DerivedSigningPubkey, InvoiceBuilder};
@@ -167,6 +167,25 @@ pub(crate) enum PendingOutboundPayment {
167167
},
168168
}
169169

170+
#[derive(Clone, Eq, PartialEq)]
171+
pub(crate) struct NextTrampolineHopInfo {
172+
/// The Trampoline packet to include for the next Trampoline hop.
173+
pub(crate) onion_packet: TrampolineOnionPacket,
174+
/// If blinded, the current_path_key to set at the next Trampoline hop.
175+
pub(crate) blinding_point: Option<PublicKey>,
176+
/// The amount that the next trampoline is expecting to receive.
177+
pub(crate) amount_msat: u64,
178+
/// The cltv expiry height that the next trampoline is expecting.
179+
pub(crate) cltv_expiry_height: u32,
180+
}
181+
182+
impl_writeable_tlv_based!(NextTrampolineHopInfo, {
183+
(1, onion_packet, required),
184+
(3, blinding_point, option),
185+
(5, amount_msat, required),
186+
(7, cltv_expiry_height, required),
187+
});
188+
170189
#[derive(Clone)]
171190
pub(crate) struct RetryableInvoiceRequest {
172191
pub(crate) invoice_request: InvoiceRequest,

0 commit comments

Comments
 (0)