Skip to content

Commit 11bf180

Browse files
committed
Move JIT parameters to BOLT11 payment_metadata
`LSPS2Parameters` should travel with generated JIT invoices and the returned recipient onion metadata instead of living in a dedicated `PaymentKind::Bolt11Jit` payment-store variant. Store new JIT payments as `PaymentKind::Bolt11`, decode legacy `PaymentKind::Bolt11Jit` records as `PaymentKind::Bolt11`, and reject nonzero `counterparty_skimmed_fee_msat` unless valid BOLT11 `payment_metadata` proves the `LSPS2` fee is allowed. Co-Authored-By: HAL 9000
1 parent 77172d2 commit 11bf180

7 files changed

Lines changed: 294 additions & 136 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
# Pending
2+
3+
## Compatibility Notes
4+
- Pending JIT-channel payments created before upgrading may fail after upgrade because the
5+
prior LSPS2 fee-limit state stored in `PaymentKind::Bolt11Jit` is not migrated.
6+
17
# 0.7.0 - Dec. 3, 2025
28
This seventh minor release introduces numerous new features, bug fixes, and API improvements. In particular, it adds support for channel Splicing, Async Payments, as well as sourcing chain data from a Bitcoin Core REST backend.
39

@@ -419,4 +425,3 @@ integrated LDK and BDK-based wallets.
419425

420426
**Note:** This release is still considered experimental, should not be run in
421427
production, and no compatibility guarantees are given until the release of 0.1.
422-

src/event.rs

Lines changed: 101 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ use crate::payment::asynchronous::static_invoice_store::StaticInvoiceStore;
5050
use crate::payment::store::{
5151
PaymentDetails, PaymentDetailsUpdate, PaymentDirection, PaymentKind, PaymentStatus,
5252
};
53+
use crate::payment::Bolt11PaymentMetadata;
5354
use crate::runtime::Runtime;
5455
use crate::types::{
5556
CustomTlvRecord, DynStore, KeysManager, OnionMessenger, PaymentStore, Sweeper, Wallet,
@@ -581,6 +582,35 @@ where
581582
}
582583
}
583584

585+
fn fail_claimable_payment(
586+
&self, payment_id: PaymentId, payment_hash: &PaymentHash,
587+
) -> Result<(), ReplayEvent> {
588+
self.channel_manager.fail_htlc_backwards(payment_hash);
589+
590+
let update = PaymentDetailsUpdate {
591+
status: Some(PaymentStatus::Failed),
592+
..PaymentDetailsUpdate::new(payment_id)
593+
};
594+
match self.payment_store.update(update) {
595+
Ok(_) => Ok(()),
596+
Err(e) => {
597+
log_error!(self.logger, "Failed to access payment store: {}", e);
598+
Err(ReplayEvent())
599+
},
600+
}
601+
}
602+
603+
fn lsps2_max_total_opening_fee_msat(payment_metadata: &[u8], amount_msat: u64) -> Option<u64> {
604+
let metadata = Bolt11PaymentMetadata::read(&mut &payment_metadata[..]).ok()?;
605+
let lsps2_parameters = metadata.lsps2_parameters?;
606+
lsps2_parameters.max_total_opening_fee_msat.or_else(|| {
607+
lsps2_parameters.max_proportional_opening_fee_ppm_msat.and_then(|max_prop_fee| {
608+
// If it's a variable amount payment, compute the actual fee.
609+
compute_opening_fee(amount_msat, 0, max_prop_fee)
610+
})
611+
})
612+
}
613+
584614
pub async fn handle_event(&self, event: LdkEvent) -> Result<(), ReplayEvent> {
585615
match event {
586616
LdkEvent::FundingGenerationReady {
@@ -694,7 +724,8 @@ where
694724
..
695725
} => {
696726
let payment_id = PaymentId(payment_hash.0);
697-
if let Some(info) = self.payment_store.get(&payment_id) {
727+
let payment_info = self.payment_store.get(&payment_id);
728+
if let Some(info) = payment_info.as_ref() {
698729
if info.direction == PaymentDirection::Outbound {
699730
log_info!(
700731
self.logger,
@@ -717,14 +748,13 @@ where
717748
}
718749

719750
if info.status == PaymentStatus::Succeeded
720-
|| matches!(info.kind, PaymentKind::Spontaneous { .. })
751+
|| matches!(&info.kind, PaymentKind::Spontaneous { .. })
721752
{
722-
let stored_preimage = match info.kind {
753+
let stored_preimage = match &info.kind {
723754
PaymentKind::Bolt11 { preimage, .. }
724-
| PaymentKind::Bolt11Jit { preimage, .. }
725755
| PaymentKind::Bolt12Offer { preimage, .. }
726756
| PaymentKind::Bolt12Refund { preimage, .. }
727-
| PaymentKind::Spontaneous { preimage, .. } => preimage,
757+
| PaymentKind::Spontaneous { preimage, .. } => *preimage,
728758
_ => None,
729759
};
730760

@@ -759,22 +789,28 @@ where
759789
},
760790
};
761791
}
792+
}
762793

763-
let max_total_opening_fee_msat = match info.kind {
764-
PaymentKind::Bolt11Jit { lsp_fee_limits, .. } => {
765-
lsp_fee_limits
766-
.max_total_opening_fee_msat
767-
.or_else(|| {
768-
lsp_fee_limits.max_proportional_opening_fee_ppm_msat.and_then(
769-
|max_prop_fee| {
770-
// If it's a variable amount payment, compute the actual fee.
771-
compute_opening_fee(amount_msat, 0, max_prop_fee)
772-
},
773-
)
774-
})
775-
.unwrap_or(0)
776-
},
777-
_ => 0,
794+
if counterparty_skimmed_fee_msat > 0 {
795+
let max_total_opening_fee_msat = match &purpose {
796+
PaymentPurpose::Bolt11InvoicePayment { .. } => onion_fields
797+
.as_ref()
798+
.and_then(|fields| fields.payment_metadata.as_ref())
799+
.and_then(|metadata| {
800+
Self::lsps2_max_total_opening_fee_msat(metadata, amount_msat)
801+
}),
802+
_ => None,
803+
};
804+
805+
let Some(max_total_opening_fee_msat) = max_total_opening_fee_msat else {
806+
log_info!(
807+
self.logger,
808+
"Refusing inbound payment with hash {} as the counterparty withheld {}msat without valid BOLT11 LSPS2 payment metadata",
809+
hex_utils::to_string(&payment_hash.0),
810+
counterparty_skimmed_fee_msat,
811+
);
812+
self.fail_claimable_payment(payment_id, &payment_hash)?;
813+
return Ok(());
778814
};
779815

780816
if counterparty_skimmed_fee_msat > max_total_opening_fee_msat {
@@ -785,26 +821,13 @@ where
785821
counterparty_skimmed_fee_msat,
786822
max_total_opening_fee_msat,
787823
);
788-
self.channel_manager.fail_htlc_backwards(&payment_hash);
789-
790-
let update = PaymentDetailsUpdate {
791-
hash: Some(Some(payment_hash)),
792-
status: Some(PaymentStatus::Failed),
793-
..PaymentDetailsUpdate::new(payment_id)
794-
};
795-
match self.payment_store.update(update) {
796-
Ok(_) => return Ok(()),
797-
Err(e) => {
798-
log_error!(self.logger, "Failed to access payment store: {}", e);
799-
return Err(ReplayEvent());
800-
},
801-
};
824+
self.fail_claimable_payment(payment_id, &payment_hash)?;
825+
return Ok(());
802826
}
803827

804-
// If the LSP skimmed anything, update our stored payment.
805-
if counterparty_skimmed_fee_msat > 0 {
806-
match info.kind {
807-
PaymentKind::Bolt11Jit { .. } => {
828+
if let Some(info) = payment_info.as_ref() {
829+
match &info.kind {
830+
PaymentKind::Bolt11 { .. } => {
808831
let update = PaymentDetailsUpdate {
809832
counterparty_skimmed_fee_msat: Some(Some(counterparty_skimmed_fee_msat)),
810833
..PaymentDetailsUpdate::new(payment_id)
@@ -817,16 +840,17 @@ where
817840
},
818841
};
819842
}
820-
_ => debug_assert!(false, "We only expect the counterparty to get away with withholding fees for JIT payments."),
843+
_ => debug_assert!(false, "We only expect the counterparty to get away with withholding fees for BOLT11 payments."),
821844
}
822845
}
846+
}
823847

848+
if let Some(info) = payment_info {
824849
// If this is known by the store but ChannelManager doesn't know the preimage,
825850
// the payment has been registered via `_for_hash` variants and needs to be manually claimed via
826851
// user interaction.
827852
match info.kind {
828-
PaymentKind::Bolt11 { preimage, .. }
829-
| PaymentKind::Bolt11Jit { preimage, .. } => {
853+
PaymentKind::Bolt11 { preimage, .. } => {
830854
if purpose.preimage().is_none() {
831855
debug_assert!(
832856
preimage.is_none(),
@@ -1897,8 +1921,44 @@ mod tests {
18971921

18981922
use super::*;
18991923
use crate::io::test_utils::InMemoryStore;
1924+
use crate::payment::store::LSPS2Parameters;
19001925
use crate::types::DynStoreWrapper;
19011926

1927+
#[test]
1928+
fn lsps2_payment_metadata_decodes_total_fee_limit() {
1929+
let metadata = Bolt11PaymentMetadata {
1930+
lsps2_parameters: Some(LSPS2Parameters {
1931+
max_total_opening_fee_msat: Some(42_000),
1932+
max_proportional_opening_fee_ppm_msat: None,
1933+
}),
1934+
};
1935+
1936+
assert_eq!(
1937+
EventHandler::<Arc<TestLogger>>::lsps2_max_total_opening_fee_msat(
1938+
&metadata.encode(),
1939+
100_000
1940+
),
1941+
Some(42_000)
1942+
);
1943+
}
1944+
1945+
#[test]
1946+
fn lsps2_payment_metadata_missing_or_malformed_limit_is_rejected() {
1947+
let empty_metadata = Bolt11PaymentMetadata { lsps2_parameters: None }.encode();
1948+
1949+
assert_eq!(
1950+
EventHandler::<Arc<TestLogger>>::lsps2_max_total_opening_fee_msat(
1951+
&empty_metadata,
1952+
100_000
1953+
),
1954+
None
1955+
);
1956+
assert_eq!(
1957+
EventHandler::<Arc<TestLogger>>::lsps2_max_total_opening_fee_msat(&[0xff], 100_000),
1958+
None
1959+
);
1960+
}
1961+
19021962
#[tokio::test]
19031963
async fn event_queue_persistence() {
19041964
let store: Arc<DynStore> = Arc::new(DynStoreWrapper(InMemoryStore::new()));

src/liquidity.rs

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ use lightning::ln::msgs::SocketAddress;
2121
use lightning::ln::types::ChannelId;
2222
use lightning::routing::router::{RouteHint, RouteHintHop};
2323
use lightning::sign::EntropySource;
24+
use lightning::util::ser::Writeable;
2425
use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription, InvoiceBuilder, RoutingFees};
2526
use lightning_liquidity::events::LiquidityEvent;
2627
use lightning_liquidity::lsps0::ser::{LSPSDateTime, LSPSRequestId};
@@ -41,6 +42,8 @@ use tokio::sync::oneshot;
4142
use crate::builder::BuildError;
4243
use crate::connection::ConnectionManager;
4344
use crate::logger::{log_debug, log_error, log_info, LdkLogger, Logger};
45+
use crate::payment::store::LSPS2Parameters;
46+
use crate::payment::Bolt11PaymentMetadata;
4447
use crate::runtime::Runtime;
4548
use crate::types::{
4649
Broadcaster, ChannelManager, DynStore, KeysManager, LiquidityManager, PeerManager, Wallet,
@@ -1113,7 +1116,7 @@ where
11131116
pub(crate) async fn lsps2_receive_to_jit_channel(
11141117
&self, amount_msat: u64, description: &Bolt11InvoiceDescription, expiry_secs: u32,
11151118
max_total_lsp_fee_limit_msat: Option<u64>, payment_hash: Option<PaymentHash>,
1116-
) -> Result<(Bolt11Invoice, u64), Error> {
1119+
) -> Result<Bolt11Invoice, Error> {
11171120
let fee_response = self.lsps2_request_opening_fee_params().await?;
11181121

11191122
let (min_total_fee_msat, min_opening_params) = fee_response
@@ -1159,22 +1162,27 @@ where
11591162

11601163
let buy_response =
11611164
self.lsps2_send_buy_request(Some(amount_msat), min_opening_params).await?;
1165+
let lsps2_parameters = LSPS2Parameters {
1166+
max_total_opening_fee_msat: Some(min_total_fee_msat),
1167+
max_proportional_opening_fee_ppm_msat: None,
1168+
};
11621169
let invoice = self.lsps2_create_jit_invoice(
11631170
buy_response,
11641171
Some(amount_msat),
11651172
description,
11661173
expiry_secs,
11671174
payment_hash,
1175+
lsps2_parameters,
11681176
)?;
11691177

11701178
log_info!(self.logger, "JIT-channel invoice created: {}", invoice);
1171-
Ok((invoice, min_total_fee_msat))
1179+
Ok(invoice)
11721180
}
11731181

11741182
pub(crate) async fn lsps2_receive_variable_amount_to_jit_channel(
11751183
&self, description: &Bolt11InvoiceDescription, expiry_secs: u32,
11761184
max_proportional_lsp_fee_limit_ppm_msat: Option<u64>, payment_hash: Option<PaymentHash>,
1177-
) -> Result<(Bolt11Invoice, u64), Error> {
1185+
) -> Result<Bolt11Invoice, Error> {
11781186
let fee_response = self.lsps2_request_opening_fee_params().await?;
11791187

11801188
let (min_prop_fee_ppm_msat, min_opening_params) = fee_response
@@ -1207,16 +1215,21 @@ where
12071215
);
12081216

12091217
let buy_response = self.lsps2_send_buy_request(None, min_opening_params).await?;
1218+
let lsps2_parameters = LSPS2Parameters {
1219+
max_total_opening_fee_msat: None,
1220+
max_proportional_opening_fee_ppm_msat: Some(min_prop_fee_ppm_msat),
1221+
};
12101222
let invoice = self.lsps2_create_jit_invoice(
12111223
buy_response,
12121224
None,
12131225
description,
12141226
expiry_secs,
12151227
payment_hash,
1228+
lsps2_parameters,
12161229
)?;
12171230

12181231
log_info!(self.logger, "JIT-channel invoice created: {}", invoice);
1219-
Ok((invoice, min_prop_fee_ppm_msat))
1232+
Ok(invoice)
12201233
}
12211234

12221235
async fn lsps2_request_opening_fee_params(&self) -> Result<LSPS2FeeResponse, Error> {
@@ -1298,7 +1311,7 @@ where
12981311
fn lsps2_create_jit_invoice(
12991312
&self, buy_response: LSPS2BuyResponse, amount_msat: Option<u64>,
13001313
description: &Bolt11InvoiceDescription, expiry_secs: u32,
1301-
payment_hash: Option<PaymentHash>,
1314+
payment_hash: Option<PaymentHash>, lsps2_parameters: LSPS2Parameters,
13021315
) -> Result<Bolt11Invoice, Error> {
13031316
let lsps2_client = self.lsps2_client.as_ref().ok_or(Error::LiquiditySourceUnavailable)?;
13041317

@@ -1346,7 +1359,11 @@ where
13461359
.current_timestamp()
13471360
.min_final_cltv_expiry_delta(min_final_cltv_expiry_delta.into())
13481361
.expiry_time(Duration::from_secs(expiry_secs.into()))
1349-
.private_route(route_hint);
1362+
.private_route(route_hint)
1363+
.payment_metadata(
1364+
Bolt11PaymentMetadata { lsps2_parameters: Some(lsps2_parameters) }.encode(),
1365+
)
1366+
.require_payment_metadata();
13501367

13511368
if let Some(amount_msat) = amount_msat {
13521369
invoice_builder = invoice_builder.amount_milli_satoshis(amount_msat).basic_mpp();

0 commit comments

Comments
 (0)