Skip to content

Commit b8e48ac

Browse files
authored
Merge pull request #3757 from TheBlueMatt/2025-04-0.1.3-backports
Backports for 0.1.3
2 parents 9069cc3 + 4deb527 commit b8e48ac

File tree

10 files changed

+427
-84
lines changed

10 files changed

+427
-84
lines changed

ci/ci-tests.sh

+3
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ PIN_RELEASE_DEPS # pin the release dependencies in our main workspace
2121
# The addr2line v0.21 crate (a dependency of `backtrace` starting with 0.3.69) relies on rustc 1.65
2222
[ "$RUSTC_MINOR_VERSION" -lt 65 ] && cargo update -p backtrace --precise "0.3.68" --verbose
2323

24+
# The once_cell v1.21.0 crate (a dependency of `proptest`) relies on rustc 1.70
25+
[ "$RUSTC_MINOR_VERSION" -lt 70 ] && cargo update -p once_cell --precise "1.20.3" --verbose
26+
2427
# proptest 1.3.0 requires rustc 1.64.0
2528
[ "$RUSTC_MINOR_VERSION" -lt 64 ] && cargo update -p proptest --precise "1.2.0" --verbose
2629

fuzz/src/invoice_request_deser.rs

+18-8
Original file line numberDiff line numberDiff line change
@@ -85,16 +85,26 @@ fn build_response<T: secp256k1::Signing + secp256k1::Verification>(
8585
let expanded_key = ExpandedKey::new([42; 32]);
8686
let entropy_source = Randomness {};
8787
let nonce = Nonce::from_entropy_source(&entropy_source);
88+
89+
let invoice_request_fields =
90+
if let Ok(ver) = invoice_request.clone().verify_using_metadata(&expanded_key, secp_ctx) {
91+
// Previously we had a panic where we'd truncate the payer note possibly cutting a
92+
// Unicode character in two here, so try to fetch fields if we can validate.
93+
ver.fields()
94+
} else {
95+
InvoiceRequestFields {
96+
payer_signing_pubkey: invoice_request.payer_signing_pubkey(),
97+
quantity: invoice_request.quantity(),
98+
payer_note_truncated: invoice_request
99+
.payer_note()
100+
.map(|s| UntrustedString(s.to_string())),
101+
human_readable_name: None,
102+
}
103+
};
104+
88105
let payment_context = PaymentContext::Bolt12Offer(Bolt12OfferContext {
89106
offer_id: OfferId([42; 32]),
90-
invoice_request: InvoiceRequestFields {
91-
payer_signing_pubkey: invoice_request.payer_signing_pubkey(),
92-
quantity: invoice_request.quantity(),
93-
payer_note_truncated: invoice_request
94-
.payer_note()
95-
.map(|s| UntrustedString(s.to_string())),
96-
human_readable_name: None,
97-
},
107+
invoice_request: invoice_request_fields,
98108
});
99109
let payee_tlvs = UnauthenticatedReceiveTlvs {
100110
payment_secret: PaymentSecret([42; 32]),

lightning/src/blinded_path/payment.rs

+34-15
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ use crate::offers::nonce::Nonce;
3030
use crate::offers::offer::OfferId;
3131
use crate::routing::gossip::{NodeId, ReadOnlyNetworkGraph};
3232
use crate::sign::{EntropySource, NodeSigner, Recipient};
33+
use crate::types::routing::RoutingFees;
3334
use crate::util::ser::{FixedLengthReader, LengthReadableArgs, HighZeroBytesDroppedBigSize, Readable, WithoutLength, Writeable, Writer};
3435

3536
use core::mem;
@@ -530,20 +531,17 @@ pub(crate) fn amt_to_forward_msat(inbound_amt_msat: u64, payment_relay: &Payment
530531
u64::try_from(amt_to_forward).ok()
531532
}
532533

533-
pub(super) fn compute_payinfo(
534-
intermediate_nodes: &[PaymentForwardNode], payee_tlvs: &UnauthenticatedReceiveTlvs,
535-
payee_htlc_maximum_msat: u64, min_final_cltv_expiry_delta: u16,
536-
) -> Result<BlindedPayInfo, ()> {
534+
// Returns (aggregated_base_fee, aggregated_proportional_fee)
535+
pub(crate) fn compute_aggregated_base_prop_fee<I>(hops_fees: I) -> Result<(u64, u64), ()>
536+
where
537+
I: DoubleEndedIterator<Item = RoutingFees>,
538+
{
537539
let mut curr_base_fee: u64 = 0;
538540
let mut curr_prop_mil: u64 = 0;
539-
let mut cltv_expiry_delta: u16 = min_final_cltv_expiry_delta;
540-
for tlvs in intermediate_nodes.iter().rev().map(|n| &n.tlvs) {
541-
// In the future, we'll want to take the intersection of all supported features for the
542-
// `BlindedPayInfo`, but there are no features in that context right now.
543-
if tlvs.features.requires_unknown_bits_from(&BlindedHopFeatures::empty()) { return Err(()) }
541+
for fees in hops_fees.rev() {
542+
let next_base_fee = fees.base_msat as u64;
543+
let next_prop_mil = fees.proportional_millionths as u64;
544544

545-
let next_base_fee = tlvs.payment_relay.fee_base_msat as u64;
546-
let next_prop_mil = tlvs.payment_relay.fee_proportional_millionths as u64;
547545
// Use integer arithmetic to compute `ceil(a/b)` as `(a+b-1)/b`
548546
// ((curr_base_fee * (1_000_000 + next_prop_mil)) / 1_000_000) + next_base_fee
549547
curr_base_fee = curr_base_fee.checked_mul(1_000_000 + next_prop_mil)
@@ -558,13 +556,34 @@ pub(super) fn compute_payinfo(
558556
.map(|f| f / 1_000_000)
559557
.and_then(|f| f.checked_sub(1_000_000))
560558
.ok_or(())?;
561-
562-
cltv_expiry_delta = cltv_expiry_delta.checked_add(tlvs.payment_relay.cltv_expiry_delta).ok_or(())?;
563559
}
564560

561+
Ok((curr_base_fee, curr_prop_mil))
562+
}
563+
564+
pub(super) fn compute_payinfo(
565+
intermediate_nodes: &[PaymentForwardNode], payee_tlvs: &UnauthenticatedReceiveTlvs,
566+
payee_htlc_maximum_msat: u64, min_final_cltv_expiry_delta: u16,
567+
) -> Result<BlindedPayInfo, ()> {
568+
let (aggregated_base_fee, aggregated_prop_fee) =
569+
compute_aggregated_base_prop_fee(intermediate_nodes.iter().map(|node| RoutingFees {
570+
base_msat: node.tlvs.payment_relay.fee_base_msat,
571+
proportional_millionths: node.tlvs.payment_relay.fee_proportional_millionths,
572+
}))?;
573+
565574
let mut htlc_minimum_msat: u64 = 1;
566575
let mut htlc_maximum_msat: u64 = 21_000_000 * 100_000_000 * 1_000; // Total bitcoin supply
576+
let mut cltv_expiry_delta: u16 = min_final_cltv_expiry_delta;
567577
for node in intermediate_nodes.iter() {
578+
// In the future, we'll want to take the intersection of all supported features for the
579+
// `BlindedPayInfo`, but there are no features in that context right now.
580+
if node.tlvs.features.requires_unknown_bits_from(&BlindedHopFeatures::empty()) {
581+
return Err(());
582+
}
583+
584+
cltv_expiry_delta =
585+
cltv_expiry_delta.checked_add(node.tlvs.payment_relay.cltv_expiry_delta).ok_or(())?;
586+
568587
// The min htlc for an intermediate node is that node's min minus the fees charged by all of the
569588
// following hops for forwarding that min, since that fee amount will automatically be included
570589
// in the amount that this node receives and contribute towards reaching its min.
@@ -583,8 +602,8 @@ pub(super) fn compute_payinfo(
583602

584603
if htlc_maximum_msat < htlc_minimum_msat { return Err(()) }
585604
Ok(BlindedPayInfo {
586-
fee_base_msat: u32::try_from(curr_base_fee).map_err(|_| ())?,
587-
fee_proportional_millionths: u32::try_from(curr_prop_mil).map_err(|_| ())?,
605+
fee_base_msat: u32::try_from(aggregated_base_fee).map_err(|_| ())?,
606+
fee_proportional_millionths: u32::try_from(aggregated_prop_fee).map_err(|_| ())?,
588607
cltv_expiry_delta,
589608
htlc_minimum_msat,
590609
htlc_maximum_msat,

lightning/src/ln/channelmanager.rs

+5
Original file line numberDiff line numberDiff line change
@@ -12148,6 +12148,11 @@ where
1214812148
);
1214912149

1215012150
if self.default_configuration.manually_handle_bolt12_invoices {
12151+
// Update the corresponding entry in `PendingOutboundPayment` for this invoice.
12152+
// This ensures that event generation remains idempotent in case we receive
12153+
// the same invoice multiple times.
12154+
self.pending_outbound_payments.mark_invoice_received(&invoice, payment_id).ok()?;
12155+
1215112156
let event = Event::InvoiceReceived {
1215212157
payment_id, invoice, context, responder,
1215312158
};

lightning/src/ln/offers_tests.rs

+8-1
Original file line numberDiff line numberDiff line change
@@ -1185,7 +1185,14 @@ fn pays_bolt12_invoice_asynchronously() {
11851185
let onion_message = alice.onion_messenger.next_onion_message_for_peer(bob_id).unwrap();
11861186
bob.onion_messenger.handle_onion_message(alice_id, &onion_message);
11871187

1188-
let (invoice, context) = match get_event!(bob, Event::InvoiceReceived) {
1188+
// Re-process the same onion message to ensure idempotency —
1189+
// we should not generate a duplicate `InvoiceReceived` event.
1190+
bob.onion_messenger.handle_onion_message(alice_id, &onion_message);
1191+
1192+
let mut events = bob.node.get_and_clear_pending_events();
1193+
assert_eq!(events.len(), 1);
1194+
1195+
let (invoice, context) = match events.pop().unwrap() {
11891196
Event::InvoiceReceived { payment_id: actual_payment_id, invoice, context, .. } => {
11901197
assert_eq!(actual_payment_id, payment_id);
11911198
(invoice, context)

lightning/src/ln/outbound_payment.rs

+50-23
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,9 @@ pub(crate) enum PendingOutboundPayment {
7373
max_total_routing_fee_msat: Option<u64>,
7474
retryable_invoice_request: Option<RetryableInvoiceRequest>
7575
},
76-
// This state will never be persisted to disk because we transition from `AwaitingInvoice` to
77-
// `Retryable` atomically within the `ChannelManager::total_consistency_lock`. Useful to avoid
78-
// holding the `OutboundPayments::pending_outbound_payments` lock during pathfinding.
76+
// Represents the state after the invoice has been received, transitioning from the corresponding
77+
// `AwaitingInvoice` state.
78+
// Helps avoid holding the `OutboundPayments::pending_outbound_payments` lock during pathfinding.
7979
InvoiceReceived {
8080
payment_hash: PaymentHash,
8181
retry_strategy: Retry,
@@ -833,26 +833,8 @@ impl OutboundPayments {
833833
IH: Fn() -> InFlightHtlcs,
834834
SP: Fn(SendAlongPathArgs) -> Result<(), APIError>,
835835
{
836-
let payment_hash = invoice.payment_hash();
837-
let max_total_routing_fee_msat;
838-
let retry_strategy;
839-
match self.pending_outbound_payments.lock().unwrap().entry(payment_id) {
840-
hash_map::Entry::Occupied(entry) => match entry.get() {
841-
PendingOutboundPayment::AwaitingInvoice {
842-
retry_strategy: retry, max_total_routing_fee_msat: max_total_fee, ..
843-
} => {
844-
retry_strategy = *retry;
845-
max_total_routing_fee_msat = *max_total_fee;
846-
*entry.into_mut() = PendingOutboundPayment::InvoiceReceived {
847-
payment_hash,
848-
retry_strategy: *retry,
849-
max_total_routing_fee_msat,
850-
};
851-
},
852-
_ => return Err(Bolt12PaymentError::DuplicateInvoice),
853-
},
854-
hash_map::Entry::Vacant(_) => return Err(Bolt12PaymentError::UnexpectedInvoice),
855-
}
836+
let (payment_hash, retry_strategy, max_total_routing_fee_msat, _) = self
837+
.mark_invoice_received_and_get_details(invoice, payment_id)?;
856838

857839
if invoice.invoice_features().requires_unknown_bits_from(&features) {
858840
self.abandon_payment(
@@ -1754,6 +1736,51 @@ impl OutboundPayments {
17541736
}
17551737
}
17561738

1739+
pub(super) fn mark_invoice_received(
1740+
&self, invoice: &Bolt12Invoice, payment_id: PaymentId
1741+
) -> Result<(), Bolt12PaymentError> {
1742+
self.mark_invoice_received_and_get_details(invoice, payment_id)
1743+
.and_then(|(_, _, _, is_newly_marked)| {
1744+
is_newly_marked
1745+
.then_some(())
1746+
.ok_or(Bolt12PaymentError::DuplicateInvoice)
1747+
})
1748+
}
1749+
1750+
fn mark_invoice_received_and_get_details(
1751+
&self, invoice: &Bolt12Invoice, payment_id: PaymentId
1752+
) -> Result<(PaymentHash, Retry, Option<u64>, bool), Bolt12PaymentError> {
1753+
match self.pending_outbound_payments.lock().unwrap().entry(payment_id) {
1754+
hash_map::Entry::Occupied(entry) => match entry.get() {
1755+
PendingOutboundPayment::AwaitingInvoice {
1756+
retry_strategy: retry, max_total_routing_fee_msat: max_total_fee, ..
1757+
} => {
1758+
let payment_hash = invoice.payment_hash();
1759+
let retry = *retry;
1760+
let max_total_fee = *max_total_fee;
1761+
*entry.into_mut() = PendingOutboundPayment::InvoiceReceived {
1762+
payment_hash,
1763+
retry_strategy: retry,
1764+
max_total_routing_fee_msat: max_total_fee,
1765+
};
1766+
1767+
Ok((payment_hash, retry, max_total_fee, true))
1768+
},
1769+
// When manual invoice handling is enabled, the corresponding `PendingOutboundPayment` entry
1770+
// is already updated at the time the invoice is received. This ensures that `InvoiceReceived`
1771+
// event generation remains idempotent, even if the same invoice is received again before the
1772+
// event is handled by the user.
1773+
PendingOutboundPayment::InvoiceReceived {
1774+
retry_strategy, max_total_routing_fee_msat, ..
1775+
} => {
1776+
Ok((invoice.payment_hash(), *retry_strategy, *max_total_routing_fee_msat, false))
1777+
},
1778+
_ => Err(Bolt12PaymentError::DuplicateInvoice),
1779+
},
1780+
hash_map::Entry::Vacant(_) => Err(Bolt12PaymentError::UnexpectedInvoice),
1781+
}
1782+
}
1783+
17571784
fn pay_route_internal<NS: Deref, F>(
17581785
&self, route: &Route, payment_hash: PaymentHash, recipient_onion: &RecipientOnionFields,
17591786
keysend_preimage: Option<PaymentPreimage>, invoice_request: Option<&InvoiceRequest>,

lightning/src/ln/payment_tests.rs

+52
Original file line numberDiff line numberDiff line change
@@ -4479,3 +4479,55 @@ fn pay_route_without_params() {
44794479
ClaimAlongRouteArgs::new(&nodes[0], &[&[&nodes[1]]], payment_preimage)
44804480
);
44814481
}
4482+
4483+
#[test]
4484+
fn max_out_mpp_path() {
4485+
// In this setup, the sender is attempting to route an MPP payment split across the two channels
4486+
// that it has with its LSP, where the LSP has a single large channel to the recipient.
4487+
//
4488+
// Previously a user ran into a pathfinding failure here because our router was not sending the
4489+
// maximum possible value over the first MPP path it found due to overestimating the fees needed
4490+
// to cover the following hops. Because the path that had just been found was not maxxed out, our
4491+
// router assumed that we had already found enough paths to cover the full payment amount and that
4492+
// we were finding additional paths for the purpose of redundant path selection. This caused the
4493+
// router to mark the recipient's only channel as exhausted, with the intention of choosing more
4494+
// unique paths in future iterations. In reality, this ended up with the recipient's only channel
4495+
// being disabled and subsequently failing to find a route entirely.
4496+
//
4497+
// The router has since been updated to fully utilize the capacity of any paths it finds in this
4498+
// situation, preventing the "redundant path selection" behavior from kicking in.
4499+
4500+
let mut user_cfg = test_default_channel_config();
4501+
user_cfg.channel_config.forwarding_fee_base_msat = 0;
4502+
user_cfg.channel_handshake_config.max_inbound_htlc_value_in_flight_percent_of_channel = 100;
4503+
let mut lsp_cfg = test_default_channel_config();
4504+
lsp_cfg.channel_config.forwarding_fee_base_msat = 0;
4505+
lsp_cfg.channel_config.forwarding_fee_proportional_millionths = 3000;
4506+
lsp_cfg.channel_handshake_config.max_inbound_htlc_value_in_flight_percent_of_channel = 100;
4507+
4508+
let chanmon_cfgs = create_chanmon_cfgs(3);
4509+
let node_cfgs = create_node_cfgs(3, &chanmon_cfgs);
4510+
let node_chanmgrs = create_node_chanmgrs(
4511+
3, &node_cfgs, &[Some(user_cfg.clone()), Some(lsp_cfg.clone()), Some(user_cfg.clone())]
4512+
);
4513+
let nodes = create_network(3, &node_cfgs, &node_chanmgrs);
4514+
4515+
create_unannounced_chan_between_nodes_with_value(&nodes, 0, 1, 200_000, 0);
4516+
create_unannounced_chan_between_nodes_with_value(&nodes, 0, 1, 300_000, 0);
4517+
create_unannounced_chan_between_nodes_with_value(&nodes, 1, 2, 600_000, 0);
4518+
4519+
let amt_msat = 350_000_000;
4520+
let invoice_params = crate::ln::channelmanager::Bolt11InvoiceParameters {
4521+
amount_msats: Some(amt_msat),
4522+
..Default::default()
4523+
};
4524+
let invoice = nodes[2].node.create_bolt11_invoice(invoice_params).unwrap();
4525+
4526+
let (hash, onion, params) =
4527+
crate::ln::bolt11_payment::payment_parameters_from_invoice(&invoice).unwrap();
4528+
nodes[0].node.send_payment(hash, onion, PaymentId([42; 32]), params, Retry::Attempts(0)).unwrap();
4529+
4530+
assert!(nodes[0].node.list_recent_payments().len() == 1);
4531+
check_added_monitors(&nodes[0], 2); // one monitor update per MPP part
4532+
nodes[0].node.get_and_clear_pending_msg_events();
4533+
}

0 commit comments

Comments
 (0)