Skip to content

Commit 8ff7fe0

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 e1f190a commit 8ff7fe0

7 files changed

Lines changed: 312 additions & 137 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: 115 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::PaymentMetadata;
5354
use crate::runtime::Runtime;
5455
use crate::types::{
5556
CustomTlvRecord, DynStore, KeysManager, OnionMessenger, PaymentStore, Sweeper, Wallet,
@@ -580,6 +581,35 @@ where
580581
}
581582
}
582583

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

718749
if info.status == PaymentStatus::Succeeded
719-
|| matches!(info.kind, PaymentKind::Spontaneous { .. })
750+
|| matches!(&info.kind, PaymentKind::Spontaneous { .. })
720751
{
721-
let stored_preimage = match info.kind {
752+
let stored_preimage = match &info.kind {
722753
PaymentKind::Bolt11 { preimage, .. }
723-
| PaymentKind::Bolt11Jit { preimage, .. }
724754
| PaymentKind::Bolt12Offer { preimage, .. }
725755
| PaymentKind::Bolt12Refund { preimage, .. }
726-
| PaymentKind::Spontaneous { preimage, .. } => preimage,
756+
| PaymentKind::Spontaneous { preimage, .. } => *preimage,
727757
_ => None,
728758
};
729759

@@ -758,22 +788,28 @@ where
758788
},
759789
};
760790
}
791+
}
761792

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

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

803-
// If the LSP skimmed anything, update our stored payment.
804-
if counterparty_skimmed_fee_msat > 0 {
805-
match info.kind {
806-
PaymentKind::Bolt11Jit { .. } => {
827+
if let Some(info) = payment_info.as_ref() {
828+
match &info.kind {
829+
PaymentKind::Bolt11 { .. } => {
807830
let update = PaymentDetailsUpdate {
808831
counterparty_skimmed_fee_msat: Some(Some(counterparty_skimmed_fee_msat)),
809832
..PaymentDetailsUpdate::new(payment_id)
@@ -816,16 +839,17 @@ where
816839
},
817840
};
818841
}
819-
_ => debug_assert!(false, "We only expect the counterparty to get away with withholding fees for JIT payments."),
842+
_ => debug_assert!(false, "We only expect the counterparty to get away with withholding fees for BOLT11 payments."),
820843
}
821844
}
845+
}
822846

847+
if let Some(info) = payment_info {
823848
// If this is known by the store but ChannelManager doesn't know the preimage,
824849
// the payment has been registered via `_for_hash` variants and needs to be manually claimed via
825850
// user interaction.
826851
match info.kind {
827-
PaymentKind::Bolt11 { preimage, .. }
828-
| PaymentKind::Bolt11Jit { preimage, .. } => {
852+
PaymentKind::Bolt11 { preimage, .. } => {
829853
if purpose.preimage().is_none() {
830854
debug_assert!(
831855
preimage.is_none(),
@@ -1890,8 +1914,58 @@ mod tests {
18901914

18911915
use super::*;
18921916
use crate::io::test_utils::InMemoryStore;
1917+
use crate::payment::store::LSPS2Parameters;
18931918
use crate::types::DynStoreWrapper;
18941919

1920+
#[test]
1921+
fn lsps2_payment_metadata_decodes_total_fee_limit() {
1922+
let metadata = PaymentMetadata {
1923+
lsps2_parameters: Some(LSPS2Parameters {
1924+
max_total_opening_fee_msat: Some(42_000),
1925+
max_proportional_opening_fee_ppm_msat: None,
1926+
}),
1927+
};
1928+
1929+
assert_eq!(
1930+
EventHandler::<Arc<TestLogger>>::lsps2_max_total_opening_fee_msat(
1931+
&metadata.encode(),
1932+
100_000
1933+
),
1934+
Some(42_000)
1935+
);
1936+
}
1937+
1938+
#[test]
1939+
fn lsps2_payment_metadata_missing_or_malformed_limit_is_rejected() {
1940+
let empty_metadata = PaymentMetadata { lsps2_parameters: None }.encode();
1941+
let metadata_without_fee_limit = PaymentMetadata {
1942+
lsps2_parameters: Some(LSPS2Parameters {
1943+
max_total_opening_fee_msat: None,
1944+
max_proportional_opening_fee_ppm_msat: None,
1945+
}),
1946+
}
1947+
.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+
assert_eq!(
1961+
EventHandler::<Arc<TestLogger>>::lsps2_max_total_opening_fee_msat(
1962+
&metadata_without_fee_limit,
1963+
100_000
1964+
),
1965+
None
1966+
);
1967+
}
1968+
18951969
#[tokio::test]
18961970
async fn event_queue_persistence() {
18971971
let store: Arc<DynStore> = Arc::new(DynStoreWrapper(InMemoryStore::new()));

src/liquidity.rs

Lines changed: 27 additions & 7 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::PaymentMetadata;
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,12 +1311,14 @@ 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

13051318
// LSPS2 requires min_final_cltv_expiry_delta to be at least 2 more than usual.
13061319
let min_final_cltv_expiry_delta = MIN_FINAL_CLTV_EXPIRY_DELTA + 2;
1320+
let encoded_payment_metadata =
1321+
PaymentMetadata { lsps2_parameters: Some(lsps2_parameters) }.encode();
13071322
let (payment_hash, payment_secret, payment_metadata) = match payment_hash {
13081323
Some(payment_hash) => {
13091324
let (payment_secret, payment_metadata) = self
@@ -1313,7 +1328,7 @@ where
13131328
None,
13141329
expiry_secs,
13151330
Some(min_final_cltv_expiry_delta),
1316-
None,
1331+
Some(encoded_payment_metadata),
13171332
)
13181333
.map_err(|e| {
13191334
log_error!(self.logger, "Failed to register inbound payment: {:?}", e);
@@ -1323,7 +1338,12 @@ where
13231338
},
13241339
None => self
13251340
.channel_manager
1326-
.create_inbound_payment(None, expiry_secs, Some(min_final_cltv_expiry_delta), None)
1341+
.create_inbound_payment(
1342+
None,
1343+
expiry_secs,
1344+
Some(min_final_cltv_expiry_delta),
1345+
Some(encoded_payment_metadata),
1346+
)
13271347
.map_err(|e| {
13281348
log_error!(self.logger, "Failed to register inbound payment: {:?}", e);
13291349
Error::InvoiceCreationFailed

0 commit comments

Comments
 (0)