Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions bindings/ldk_node.udl
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
3 changes: 2 additions & 1 deletion src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -685,7 +685,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(),
Expand Down
46 changes: 35 additions & 11 deletions src/liquidity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -988,7 +988,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<u64>,
max_total_lsp_fee_limit_msat: Option<u64>, payment_hash: Option<PaymentHash>,
) -> Result<(Bolt11Invoice, u64), Error> {
let fee_response = self.lsps2_request_opening_fee_params().await?;

Expand Down Expand Up @@ -1040,6 +1040,7 @@ where
Some(amount_msat),
description,
expiry_secs,
payment_hash,
)?;

log_info!(self.logger, "JIT-channel invoice created: {}", invoice);
Expand All @@ -1048,7 +1049,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<u64>,
max_proportional_lsp_fee_limit_ppm_msat: Option<u64>, payment_hash: Option<PaymentHash>,
) -> Result<(Bolt11Invoice, u64), Error> {
let fee_response = self.lsps2_request_opening_fee_params().await?;

Expand Down Expand Up @@ -1082,8 +1083,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))
Expand Down Expand Up @@ -1166,18 +1172,36 @@ where
fn lsps2_create_jit_invoice(
&self, buy_response: LSPS2BuyResponse, amount_msat: Option<u64>,
description: &Bolt11InvoiceDescription, expiry_secs: u32,
payment_hash: Option<PaymentHash>,
) -> Result<Bolt11Invoice, Error> {
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,
Expand Down
98 changes: 95 additions & 3 deletions src/payment/bolt11.rs
Original file line number Diff line number Diff line change
Expand Up @@ -362,8 +362,17 @@ impl Bolt11Payment {
}

if let Some(details) = self.payment_store.get(&payment_id) {
if let Some(expected_amount_msat) = details.amount_msat {
if claimable_amount_msat < expected_amount_msat {
// For payments requested via `receive*_via_jit_channel_for_hash()`
// `skimmed_fee_msat` held by LSP must be taken into account.
let skimmed_fee_msat = match details.kind {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Mind adding a comment here to describe when this would be relevant?

PaymentKind::Bolt11Jit {
counterparty_skimmed_fee_msat: Some(skimmed_fee_msat),
..
} => skimmed_fee_msat,
_ => 0,
};
if let Some(invoice_amount_msat) = details.amount_msat {
if claimable_amount_msat < invoice_amount_msat - skimmed_fee_msat {
log_error!(
self.logger,
"Failed to manually claim payment {} as the claimable amount is less than expected",
Expand Down Expand Up @@ -580,6 +589,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<u64>, payment_hash: PaymentHash,
) -> Result<Bolt11Invoice, Error> {
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))
}
Expand All @@ -606,14 +655,55 @@ 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<u64>, payment_hash: PaymentHash,
) -> Result<Bolt11Invoice, Error> {
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))
}

fn receive_via_jit_channel_inner(
&self, amount_msat: Option<u64>, description: &LdkBolt11InvoiceDescription,
expiry_secs: u32, max_total_lsp_fee_limit_msat: Option<u64>,
max_proportional_lsp_fee_limit_ppm_msat: Option<u64>,
max_proportional_lsp_fee_limit_ppm_msat: Option<u64>, payment_hash: Option<PaymentHash>,
) -> Result<LdkBolt11Invoice, Error> {
let liquidity_source =
self.liquidity_source.as_ref().ok_or(Error::LiquiditySourceUnavailable)?;
Expand Down Expand Up @@ -645,6 +735,7 @@ impl Bolt11Payment {
description,
expiry_secs,
max_total_lsp_fee_limit_msat,
payment_hash,
)
.await
.map(|(invoice, total_fee)| (invoice, Some(total_fee), None))
Expand All @@ -654,6 +745,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)))
Expand Down
109 changes: 102 additions & 7 deletions tests/integration_tests_rust.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;
Expand Down Expand Up @@ -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());

Expand All @@ -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]
Expand Down
Loading