Skip to content

Commit 3a2864e

Browse files
committed
Merge tag 'v0.1.3' into 2025-04-0.1.3-bindings
v0.1.3 - Apr 30, 2025 - "Routing Unicode in 2025" Bug Fixes ========= * `Event::InvoiceReceived` is now only generated once for each `Bolt12Invoice` received matching a pending outbound payment. Previously it would be provided each time we received an invoice, which may happen many times if the sender sends redundant messages to improve success rates (lightningdevkit#3658). * LDK's router now more fully saturates paths which are subject to HTLC maximum restrictions after the first hop. In some rare cases this can result in finding paths when it would previously spuriously decide it cannot find enough diverse paths (lightningdevkit#3707, lightningdevkit#3755). Security ======== 0.1.3 fixes a denial-of-service vulnerability which cause a crash of an LDK-based node if an attacker has access to a valid `Bolt12Offer` which the LDK-based node created. * A malicious payer which requests a BOLT 12 Invoice from an LDK-based node (via the `Bolt12InvoiceRequest` message) can cause the panic of the LDK-based node due to the way `String::truncate` handles UTF-8 codepoints. The codepath can only be reached once the received `Botlt12InvoiceRequest` has been authenticated to be based on a valid `Bolt12Offer` which the same LDK-based node issued (lightningdevkit#3747, lightningdevkit#3750).
2 parents bbdbd6f + b4d5fe2 commit 3a2864e

File tree

12 files changed

+454
-85
lines changed

12 files changed

+454
-85
lines changed

CHANGELOG.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,27 @@
1+
# 0.1.3 - Apr 30, 2025 - "Routing Unicode in 2025"
2+
3+
## Bug Fixes
4+
* `Event::InvoiceReceived` is now only generated once for each `Bolt12Invoice`
5+
received matching a pending outbound payment. Previously it would be provided
6+
each time we received an invoice, which may happen many times if the sender
7+
sends redundant messages to improve success rates (#3658).
8+
* LDK's router now more fully saturates paths which are subject to HTLC
9+
maximum restrictions after the first hop. In some rare cases this can result
10+
in finding paths when it would previously spuriously decide it cannot find
11+
enough diverse paths (#3707, #3755).
12+
13+
## Security
14+
0.1.3 fixes a denial-of-service vulnerability which cause a crash of an
15+
LDK-based node if an attacker has access to a valid `Bolt12Offer` which the
16+
LDK-based node created.
17+
* A malicious payer which requests a BOLT 12 Invoice from an LDK-based node
18+
(via the `Bolt12InvoiceRequest` message) can cause the panic of the
19+
LDK-based node due to the way `String::truncate` handles UTF-8 codepoints.
20+
The codepath can only be reached once the received `Botlt12InvoiceRequest`
21+
has been authenticated to be based on a valid `Bolt12Offer` which the same
22+
LDK-based node issued (#3747, #3750).
23+
24+
125
# 0.1.2 - Apr 02, 2025 - "Foolishly Edgy Cases"
226

327
## API Updates
@@ -35,6 +59,7 @@
3559
vulnerable to pinning attacks if they are not yet claimable by our
3660
counterparty, potentially reducing our exposure to pinning attacks (#3564).
3761

62+
3863
# 0.1.1 - Jan 28, 2025 - "Onchain Matters"
3964

4065
## API Updates
@@ -71,6 +96,7 @@ cause force-closure of unrelated channels.
7196
when they broadcast the stale commitment (#3556). Thanks to Matt Morehouse for
7297
reporting this issue.
7398

99+
74100
# 0.1 - Jan 15, 2025 - "Human Readable Version Numbers"
75101

76102
The LDK 0.1 release represents an important milestone for the LDK project. While

ci/ci-tests.sh

Lines changed: 3 additions & 0 deletions
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

Lines changed: 18 additions & 8 deletions
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/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "lightning"
3-
version = "0.1.2"
3+
version = "0.1.3"
44
authors = ["Matt Corallo"]
55
license = "MIT OR Apache-2.0"
66
repository = "https://github.com/lightningdevkit/rust-lightning/"

lightning/src/blinded_path/payment.rs

Lines changed: 34 additions & 15 deletions
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;
@@ -529,20 +530,17 @@ pub(crate) fn amt_to_forward_msat(inbound_amt_msat: u64, payment_relay: &Payment
529530
u64::try_from(amt_to_forward).ok()
530531
}
531532

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

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

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

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

lightning/src/ln/channelmanager.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12151,6 +12151,11 @@ where
1215112151
);
1215212152

1215312153
if self.default_configuration.manually_handle_bolt12_invoices {
12154+
// Update the corresponding entry in `PendingOutboundPayment` for this invoice.
12155+
// This ensures that event generation remains idempotent in case we receive
12156+
// the same invoice multiple times.
12157+
self.pending_outbound_payments.mark_invoice_received(&invoice, payment_id).ok()?;
12158+
1215412159
let event = Event::InvoiceReceived {
1215512160
payment_id, invoice, context, responder,
1215612161
};

lightning/src/ln/offers_tests.rs

Lines changed: 8 additions & 1 deletion
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

Lines changed: 50 additions & 23 deletions
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

Lines changed: 52 additions & 0 deletions
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)