diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index 26480ca4b..9c822ae56 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -189,7 +189,11 @@ interface Bolt11Payment { [Throws=NodeError] Bolt11Invoice receive_via_jit_channel(u64 amount_msat, [ByRef]Bolt11InvoiceDescription description, u32 expiry_secs, u64? max_lsp_fee_limit_msat); [Throws=NodeError] + Bolt11Invoice receive_via_jit_channel_for_hash(u64 amount_msat, [ByRef]Bolt11InvoiceDescription description, u32 expiry_secs, u64? max_lsp_fee_limit_msat, PaymentHash payment_hash); + [Throws=NodeError] Bolt11Invoice receive_variable_amount_via_jit_channel([ByRef]Bolt11InvoiceDescription description, u32 expiry_secs, u64? max_proportional_lsp_fee_limit_ppm_msat); + [Throws=NodeError] + Bolt11Invoice receive_variable_amount_via_jit_channel_for_hash([ByRef]Bolt11InvoiceDescription description, u32 expiry_secs, u64? max_proportional_lsp_fee_limit_ppm_msat, PaymentHash payment_hash); }; interface Bolt12Payment { diff --git a/src/event.rs b/src/event.rs index 22848bec1..8ee0e2073 100644 --- a/src/event.rs +++ b/src/event.rs @@ -683,7 +683,8 @@ where // the payment has been registered via `_for_hash` variants and needs to be manually claimed via // user interaction. match info.kind { - PaymentKind::Bolt11 { preimage, .. } => { + PaymentKind::Bolt11 { preimage, .. } + | PaymentKind::Bolt11Jit { preimage, .. } => { if purpose.preimage().is_none() { debug_assert!( preimage.is_none(), diff --git a/src/liquidity.rs b/src/liquidity.rs index a4516edd0..012a5ed1c 100644 --- a/src/liquidity.rs +++ b/src/liquidity.rs @@ -987,7 +987,7 @@ where pub(crate) async fn lsps2_receive_to_jit_channel( &self, amount_msat: u64, description: &Bolt11InvoiceDescription, expiry_secs: u32, - max_total_lsp_fee_limit_msat: Option, + max_total_lsp_fee_limit_msat: Option, payment_hash: Option, ) -> Result<(Bolt11Invoice, u64), Error> { let fee_response = self.lsps2_request_opening_fee_params().await?; @@ -1039,6 +1039,7 @@ where Some(amount_msat), description, expiry_secs, + payment_hash, )?; log_info!(self.logger, "JIT-channel invoice created: {}", invoice); @@ -1047,7 +1048,7 @@ where pub(crate) async fn lsps2_receive_variable_amount_to_jit_channel( &self, description: &Bolt11InvoiceDescription, expiry_secs: u32, - max_proportional_lsp_fee_limit_ppm_msat: Option, + max_proportional_lsp_fee_limit_ppm_msat: Option, payment_hash: Option, ) -> Result<(Bolt11Invoice, u64), Error> { let fee_response = self.lsps2_request_opening_fee_params().await?; @@ -1081,8 +1082,13 @@ where ); let buy_response = self.lsps2_send_buy_request(None, min_opening_params).await?; - let invoice = - self.lsps2_create_jit_invoice(buy_response, None, description, expiry_secs)?; + let invoice = self.lsps2_create_jit_invoice( + buy_response, + None, + description, + expiry_secs, + payment_hash, + )?; log_info!(self.logger, "JIT-channel invoice created: {}", invoice); Ok((invoice, min_prop_fee_ppm_msat)) @@ -1165,18 +1171,36 @@ where fn lsps2_create_jit_invoice( &self, buy_response: LSPS2BuyResponse, amount_msat: Option, description: &Bolt11InvoiceDescription, expiry_secs: u32, + payment_hash: Option, ) -> Result { let lsps2_client = self.lsps2_client.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; // LSPS2 requires min_final_cltv_expiry_delta to be at least 2 more than usual. let min_final_cltv_expiry_delta = MIN_FINAL_CLTV_EXPIRY_DELTA + 2; - let (payment_hash, payment_secret) = self - .channel_manager - .create_inbound_payment(None, expiry_secs, Some(min_final_cltv_expiry_delta)) - .map_err(|e| { - log_error!(self.logger, "Failed to register inbound payment: {:?}", e); - Error::InvoiceCreationFailed - })?; + let (payment_hash, payment_secret) = match payment_hash { + Some(payment_hash) => { + let payment_secret = self + .channel_manager + .create_inbound_payment_for_hash( + payment_hash, + None, + expiry_secs, + Some(min_final_cltv_expiry_delta), + ) + .map_err(|e| { + log_error!(self.logger, "Failed to register inbound payment: {:?}", e); + Error::InvoiceCreationFailed + })?; + (payment_hash, payment_secret) + }, + None => self + .channel_manager + .create_inbound_payment(None, expiry_secs, Some(min_final_cltv_expiry_delta)) + .map_err(|e| { + log_error!(self.logger, "Failed to register inbound payment: {:?}", e); + Error::InvoiceCreationFailed + })?, + }; let route_hint = RouteHint(vec![RouteHintHop { src_node_id: lsps2_client.lsp_node_id, diff --git a/src/payment/bolt11.rs b/src/payment/bolt11.rs index 817a428bd..c8fcd915d 100644 --- a/src/payment/bolt11.rs +++ b/src/payment/bolt11.rs @@ -360,8 +360,15 @@ impl Bolt11Payment { } if let Some(details) = self.payment_store.get(&payment_id) { + let skimmed_fee_msat = match details.kind { + PaymentKind::Bolt11Jit { + counterparty_skimmed_fee_msat: Some(skimmed_fee_msat), + .. + } => skimmed_fee_msat, + _ => 0, + }; if let Some(expected_amount_msat) = details.amount_msat { - if claimable_amount_msat < expected_amount_msat { + if claimable_amount_msat < expected_amount_msat - skimmed_fee_msat { log_error!( self.logger, "Failed to manually claim payment {} as the claimable amount is less than expected", @@ -578,6 +585,46 @@ impl Bolt11Payment { expiry_secs, max_total_lsp_fee_limit_msat, None, + None, + )?; + Ok(maybe_wrap(invoice)) + } + + /// Returns a payable invoice that can be used to request a payment of the amount given and + /// receive it via a newly created just-in-time (JIT) channel. + /// + /// When the returned invoice is paid, the configured [LSPS2]-compliant LSP will open a channel + /// to us, supplying just-in-time inbound liquidity. + /// + /// If set, `max_total_lsp_fee_limit_msat` will limit how much fee we allow the LSP to take for opening the + /// channel to us. We'll use its cheapest offer otherwise. + /// + /// We will register the given payment hash and emit a [`PaymentClaimable`] event once + /// the inbound payment arrives. The check that [`counterparty_skimmed_fee_msat`] is within the limits + /// is performed *before* emitting the event. + /// + /// **Note:** users *MUST* handle this event and claim the payment manually via + /// [`claim_for_hash`] as soon as they have obtained access to the preimage of the given + /// payment hash. If they're unable to obtain the preimage, they *MUST* immediately fail the payment via + /// [`fail_for_hash`]. + /// + /// [LSPS2]: https://github.com/BitcoinAndLightningLayerSpecs/lsp/blob/main/LSPS2/README.md + /// [`PaymentClaimable`]: crate::Event::PaymentClaimable + /// [`claim_for_hash`]: Self::claim_for_hash + /// [`fail_for_hash`]: Self::fail_for_hash + /// [`counterparty_skimmed_fee_msat`]: crate::payment::PaymentKind::Bolt11Jit::counterparty_skimmed_fee_msat + pub fn receive_via_jit_channel_for_hash( + &self, amount_msat: u64, description: &Bolt11InvoiceDescription, expiry_secs: u32, + max_total_lsp_fee_limit_msat: Option, payment_hash: PaymentHash, + ) -> Result { + let description = maybe_try_convert_enum(description)?; + let invoice = self.receive_via_jit_channel_inner( + Some(amount_msat), + &description, + expiry_secs, + max_total_lsp_fee_limit_msat, + None, + Some(payment_hash), )?; Ok(maybe_wrap(invoice)) } @@ -604,6 +651,47 @@ impl Bolt11Payment { expiry_secs, None, max_proportional_lsp_fee_limit_ppm_msat, + None, + )?; + Ok(maybe_wrap(invoice)) + } + + /// Returns a payable invoice that can be used to request a variable amount payment (also known + /// as "zero-amount" invoice) and receive it via a newly created just-in-time (JIT) channel. + /// + /// When the returned invoice is paid, the configured [LSPS2]-compliant LSP will open a channel + /// to us, supplying just-in-time inbound liquidity. + /// + /// If set, `max_proportional_lsp_fee_limit_ppm_msat` will limit how much proportional fee, in + /// parts-per-million millisatoshis, we allow the LSP to take for opening the channel to us. + /// We'll use its cheapest offer otherwise. + /// + /// We will register the given payment hash and emit a [`PaymentClaimable`] event once + /// the inbound payment arrives. The check that [`counterparty_skimmed_fee_msat`] is within the limits + /// is performed *before* emitting the event. + /// + /// **Note:** users *MUST* handle this event and claim the payment manually via + /// [`claim_for_hash`] as soon as they have obtained access to the preimage of the given + /// payment hash. If they're unable to obtain the preimage, they *MUST* immediately fail the payment via + /// [`fail_for_hash`]. + /// + /// [LSPS2]: https://github.com/BitcoinAndLightningLayerSpecs/lsp/blob/main/LSPS2/README.md + /// [`PaymentClaimable`]: crate::Event::PaymentClaimable + /// [`claim_for_hash`]: Self::claim_for_hash + /// [`fail_for_hash`]: Self::fail_for_hash + /// [`counterparty_skimmed_fee_msat`]: crate::payment::PaymentKind::Bolt11Jit::counterparty_skimmed_fee_msat + pub fn receive_variable_amount_via_jit_channel_for_hash( + &self, description: &Bolt11InvoiceDescription, expiry_secs: u32, + max_proportional_lsp_fee_limit_ppm_msat: Option, payment_hash: PaymentHash, + ) -> Result { + let description = maybe_try_convert_enum(description)?; + let invoice = self.receive_via_jit_channel_inner( + None, + &description, + expiry_secs, + None, + max_proportional_lsp_fee_limit_ppm_msat, + Some(payment_hash), )?; Ok(maybe_wrap(invoice)) } @@ -611,7 +699,7 @@ impl Bolt11Payment { fn receive_via_jit_channel_inner( &self, amount_msat: Option, description: &LdkBolt11InvoiceDescription, expiry_secs: u32, max_total_lsp_fee_limit_msat: Option, - max_proportional_lsp_fee_limit_ppm_msat: Option, + max_proportional_lsp_fee_limit_ppm_msat: Option, payment_hash: Option, ) -> Result { let liquidity_source = self.liquidity_source.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; @@ -649,6 +737,7 @@ impl Bolt11Payment { description, expiry_secs, max_total_lsp_fee_limit_msat, + payment_hash, ) .await .map(|(invoice, total_fee)| (invoice, Some(total_fee), None)) @@ -658,6 +747,7 @@ impl Bolt11Payment { description, expiry_secs, max_proportional_lsp_fee_limit_ppm_msat, + payment_hash, ) .await .map(|(invoice, prop_fee)| (invoice, None, Some(prop_fee))) diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index 57742e09e..7951a5c44 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -9,7 +9,8 @@ mod common; use common::{ do_channel_full_cycle, expect_channel_pending_event, expect_channel_ready_event, expect_event, - expect_payment_received_event, expect_payment_successful_event, generate_blocks_and_wait, + expect_payment_claimable_event, expect_payment_received_event, expect_payment_successful_event, + generate_blocks_and_wait, logging::{init_log_logger, validate_log_entry, TestLogWriter}, open_channel, premine_and_distribute_funds, random_config, random_listening_addresses, setup_bitcoind_and_electrsd, setup_builder, setup_node, setup_two_nodes, wait_for_tx, @@ -29,7 +30,7 @@ use lightning::routing::gossip::{NodeAlias, NodeId}; use lightning::util::persist::KVStore; use lightning_invoice::{Bolt11InvoiceDescription, Description}; -use lightning_types::payment::PaymentPreimage; +use lightning_types::payment::{PaymentHash, PaymentPreimage}; use bitcoin::address::NetworkUnchecked; use bitcoin::hashes::sha256::Hash as Sha256Hash; @@ -1334,6 +1335,7 @@ fn lsps2_client_service_integration() { let payment_id = payer_node.bolt11_payment().send(&jit_invoice, None).unwrap(); expect_channel_pending_event!(service_node, client_node.node_id()); expect_channel_ready_event!(service_node, client_node.node_id()); + expect_event!(service_node, PaymentForwarded); expect_channel_pending_event!(client_node, service_node.node_id()); expect_channel_ready_event!(client_node, service_node.node_id()); @@ -1359,19 +1361,112 @@ fn lsps2_client_service_integration() { println!("Generating regular invoice!"); let invoice_description = - Bolt11InvoiceDescription::Direct(Description::new(String::from("asdf")).unwrap()); + Bolt11InvoiceDescription::Direct(Description::new(String::from("asdf")).unwrap()).into(); let amount_msat = 5_000_000; - let invoice = client_node - .bolt11_payment() - .receive(amount_msat, &invoice_description.into(), 1024) - .unwrap(); + let invoice = + client_node.bolt11_payment().receive(amount_msat, &invoice_description, 1024).unwrap(); // Have the payer_node pay the invoice, to check regular forwards service_node -> client_node // are working as expected. println!("Paying regular invoice!"); let payment_id = payer_node.bolt11_payment().send(&invoice, None).unwrap(); expect_payment_successful_event!(payer_node, Some(payment_id), None); + expect_event!(service_node, PaymentForwarded); expect_payment_received_event!(client_node, amount_msat); + + //////////////////////////////////////////////////////////////////////////// + // receive_via_jit_channel_for_hash and claim_for_hash + //////////////////////////////////////////////////////////////////////////// + println!("Generating JIT invoice!"); + // Increase the amount to make sure it does not fit into the existing channels. + let jit_amount_msat = 200_000_000; + let manual_preimage = PaymentPreimage([42u8; 32]); + let manual_payment_hash: PaymentHash = manual_preimage.into(); + let jit_invoice = client_node + .bolt11_payment() + .receive_via_jit_channel_for_hash( + jit_amount_msat, + &invoice_description, + 1024, + None, + manual_payment_hash, + ) + .unwrap(); + + // Have the payer_node pay the invoice, therby triggering channel open service_node -> client_node. + println!("Paying JIT invoice!"); + let payment_id = payer_node.bolt11_payment().send(&jit_invoice, None).unwrap(); + expect_channel_pending_event!(service_node, client_node.node_id()); + expect_channel_ready_event!(service_node, client_node.node_id()); + expect_channel_pending_event!(client_node, service_node.node_id()); + expect_channel_ready_event!(client_node, service_node.node_id()); + + let service_fee_msat = (jit_amount_msat * channel_opening_fee_ppm as u64) / 1_000_000; + let expected_received_amount_msat = jit_amount_msat - service_fee_msat; + let claimable_amount_msat = expect_payment_claimable_event!( + client_node, + payment_id, + manual_payment_hash, + expected_received_amount_msat + ); + println!("Claiming payment!"); + client_node + .bolt11_payment() + .claim_for_hash(manual_payment_hash, claimable_amount_msat, manual_preimage) + .unwrap(); + + expect_event!(service_node, PaymentForwarded); + expect_payment_successful_event!(payer_node, Some(payment_id), None); + let client_payment_id = + expect_payment_received_event!(client_node, expected_received_amount_msat).unwrap(); + let client_payment = client_node.payment(&client_payment_id).unwrap(); + match client_payment.kind { + PaymentKind::Bolt11Jit { counterparty_skimmed_fee_msat, .. } => { + assert_eq!(counterparty_skimmed_fee_msat, Some(service_fee_msat)); + }, + _ => panic!("Unexpected payment kind"), + } + + //////////////////////////////////////////////////////////////////////////// + // receive_via_jit_channel_for_hash and fail_for_hash + //////////////////////////////////////////////////////////////////////////// + println!("Generating JIT invoice!"); + // Increase the amount to make sure it does not fit into the existing channels. + let jit_amount_msat = 400_000_000; + let manual_preimage = PaymentPreimage([43u8; 32]); + let manual_payment_hash: PaymentHash = manual_preimage.into(); + let jit_invoice = client_node + .bolt11_payment() + .receive_via_jit_channel_for_hash( + jit_amount_msat, + &invoice_description, + 1024, + None, + manual_payment_hash, + ) + .unwrap(); + + // Have the payer_node pay the invoice, therby triggering channel open service_node -> client_node. + println!("Paying JIT invoice!"); + let payment_id = payer_node.bolt11_payment().send(&jit_invoice, None).unwrap(); + expect_channel_pending_event!(service_node, client_node.node_id()); + expect_channel_ready_event!(service_node, client_node.node_id()); + expect_channel_pending_event!(client_node, service_node.node_id()); + expect_channel_ready_event!(client_node, service_node.node_id()); + + let service_fee_msat = (jit_amount_msat * channel_opening_fee_ppm as u64) / 1_000_000; + let expected_received_amount_msat = jit_amount_msat - service_fee_msat; + expect_payment_claimable_event!( + client_node, + payment_id, + manual_payment_hash, + expected_received_amount_msat + ); + println!("Failing payment!"); + client_node.bolt11_payment().fail_for_hash(manual_payment_hash).unwrap(); + + expect_event!(payer_node, PaymentFailed); + assert_eq!(client_node.payment(&payment_id).unwrap().status, PaymentStatus::Failed); } #[test]