Skip to content

Simplify Bolt11 Payments and Remove Redundant Code #3617

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Mar 6, 2025
Merged
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
218 changes: 0 additions & 218 deletions lightning/src/ln/bolt11_payment.rs

This file was deleted.

159 changes: 159 additions & 0 deletions lightning/src/ln/bolt11_payment_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
// This file is Copyright its original authors, visible in version control
// history.
//
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
// You may not use this file except in accordance with one or both of these
// licenses.

//! Tests for verifying the correct end-to-end handling of BOLT11 payments, including metadata propagation.

use crate::events::Event;
use crate::ln::channelmanager::{PaymentId, Retry};
use crate::ln::functional_test_utils::*;
use crate::ln::msgs::ChannelMessageHandler;
use crate::ln::outbound_payment::Bolt11PaymentError;
use crate::routing::router::RouteParametersConfig;
use crate::sign::{NodeSigner, Recipient};
use bitcoin::hashes::sha256::Hash as Sha256;
use bitcoin::hashes::Hash;
use lightning_invoice::{Bolt11Invoice, Currency, InvoiceBuilder};
use std::time::SystemTime;

#[test]
fn payment_metadata_end_to_end_for_invoice_with_amount() {
// Test that a payment metadata read from an invoice passed to `pay_invoice` makes it all
// the way out through the `PaymentClaimable` event.
let chanmon_cfgs = create_chanmon_cfgs(2);
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]);
let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
create_announced_chan_between_nodes(&nodes, 0, 1);

let payment_metadata = vec![42, 43, 44, 45, 46, 47, 48, 49, 42];

let (payment_hash, payment_secret) =
nodes[1].node.create_inbound_payment(None, 7200, None).unwrap();

let timestamp = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap();
let invoice = InvoiceBuilder::new(Currency::Bitcoin)
.description("test".into())
.payment_hash(Sha256::from_slice(&payment_hash.0).unwrap())
.payment_secret(payment_secret)
.duration_since_epoch(timestamp)
.min_final_cltv_expiry_delta(144)
.amount_milli_satoshis(50_000)
.payment_metadata(payment_metadata.clone())
.build_raw()
.unwrap();
let sig = nodes[1].keys_manager.backing.sign_invoice(&invoice, Recipient::Node).unwrap();
let invoice = invoice.sign::<_, ()>(|_| Ok(sig)).unwrap();
let invoice = Bolt11Invoice::from_signed(invoice).unwrap();

match nodes[0].node.pay_for_bolt11_invoice(
&invoice,
PaymentId(payment_hash.0),
Some(100),
RouteParametersConfig::default(),
Retry::Attempts(0),
) {
Err(Bolt11PaymentError::InvalidAmount) => (),
_ => panic!("Unexpected result"),
};

nodes[0]
.node
.pay_for_bolt11_invoice(
&invoice,
PaymentId(payment_hash.0),
None,
RouteParametersConfig::default(),
Retry::Attempts(0),
)
.unwrap();

check_added_monitors(&nodes[0], 1);
let send_event = SendEvent::from_node(&nodes[0]);
nodes[1].node.handle_update_add_htlc(nodes[0].node.get_our_node_id(), &send_event.msgs[0]);
commitment_signed_dance!(nodes[1], nodes[0], &send_event.commitment_msg, false);

expect_pending_htlcs_forwardable!(nodes[1]);

let mut events = nodes[1].node.get_and_clear_pending_events();
assert_eq!(events.len(), 1);
match events.pop().unwrap() {
Event::PaymentClaimable { onion_fields, .. } => {
assert_eq!(Some(payment_metadata), onion_fields.unwrap().payment_metadata);
},
_ => panic!("Unexpected event"),
}
}

#[test]
fn payment_metadata_end_to_end_for_invoice_with_no_amount() {
// Test that a payment metadata read from an invoice passed to `pay_invoice` makes it all
// the way out through the `PaymentClaimable` event.
let chanmon_cfgs = create_chanmon_cfgs(2);
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]);
let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
create_announced_chan_between_nodes(&nodes, 0, 1);

let payment_metadata = vec![42, 43, 44, 45, 46, 47, 48, 49, 42];

let (payment_hash, payment_secret) =
nodes[1].node.create_inbound_payment(None, 7200, None).unwrap();

let timestamp = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap();
let invoice = InvoiceBuilder::new(Currency::Bitcoin)
.description("test".into())
.payment_hash(Sha256::from_slice(&payment_hash.0).unwrap())
.payment_secret(payment_secret)
.duration_since_epoch(timestamp)
.min_final_cltv_expiry_delta(144)
.payment_metadata(payment_metadata.clone())
.build_raw()
.unwrap();
let sig = nodes[1].keys_manager.backing.sign_invoice(&invoice, Recipient::Node).unwrap();
let invoice = invoice.sign::<_, ()>(|_| Ok(sig)).unwrap();
let invoice = Bolt11Invoice::from_signed(invoice).unwrap();

match nodes[0].node.pay_for_bolt11_invoice(
&invoice,
PaymentId(payment_hash.0),
None,
RouteParametersConfig::default(),
Retry::Attempts(0),
) {
Err(Bolt11PaymentError::InvalidAmount) => (),
_ => panic!("Unexpected result"),
};

nodes[0]
.node
.pay_for_bolt11_invoice(
&invoice,
PaymentId(payment_hash.0),
Some(50_000),
RouteParametersConfig::default(),
Retry::Attempts(0),
)
.unwrap();

check_added_monitors(&nodes[0], 1);
let send_event = SendEvent::from_node(&nodes[0]);
nodes[1].node.handle_update_add_htlc(nodes[0].node.get_our_node_id(), &send_event.msgs[0]);
commitment_signed_dance!(nodes[1], nodes[0], &send_event.commitment_msg, false);

expect_pending_htlcs_forwardable!(nodes[1]);

let mut events = nodes[1].node.get_and_clear_pending_events();
assert_eq!(events.len(), 1);
match events.pop().unwrap() {
Event::PaymentClaimable { onion_fields, .. } => {
assert_eq!(Some(payment_metadata), onion_fields.unwrap().payment_metadata);
},
_ => panic!("Unexpected event"),
}
}
53 changes: 39 additions & 14 deletions lightning/src/ln/channelmanager.rs
Original file line number Diff line number Diff line change
@@ -64,7 +64,7 @@ use crate::ln::onion_utils::{HTLCFailReason, INVALID_ONION_BLINDING};
use crate::ln::msgs::{BaseMessageHandler, ChannelMessageHandler, CommitmentUpdate, DecodeError, LightningError, MessageSendEvent};
#[cfg(test)]
use crate::ln::outbound_payment;
use crate::ln::outbound_payment::{OutboundPayments, PendingOutboundPayment, RetryableInvoiceRequest, SendAlongPathArgs, StaleExpiration};
use crate::ln::outbound_payment::{Bolt11PaymentError, OutboundPayments, PendingOutboundPayment, RetryableInvoiceRequest, SendAlongPathArgs, StaleExpiration};
use crate::offers::invoice::{Bolt12Invoice, DEFAULT_RELATIVE_EXPIRY, DerivedSigningPubkey, ExplicitSigningPubkey, InvoiceBuilder, UnsignedBolt12Invoice};
use crate::offers::invoice_error::InvoiceError;
use crate::offers::invoice_request::{InvoiceRequest, InvoiceRequestBuilder};
@@ -2038,25 +2038,23 @@ where
/// # }
/// ```
///
/// For paying an invoice, see the [`bolt11_payment`] module with convenience functions for use with
/// [`send_payment`].
///
/// ```
/// # use bitcoin::hashes::Hash;
/// # use lightning::events::{Event, EventsProvider};
/// # use lightning::types::payment::PaymentHash;
/// # use lightning::ln::channelmanager::{AChannelManager, PaymentId, RecentPaymentDetails, RecipientOnionFields, Retry};
/// # use lightning::routing::router::RouteParameters;
/// # use lightning::ln::channelmanager::{AChannelManager, PaymentId, RecentPaymentDetails, Retry};
/// # use lightning::routing::router::RouteParametersConfig;
/// # use lightning_invoice::Bolt11Invoice;
/// #
/// # fn example<T: AChannelManager>(
/// # channel_manager: T, payment_hash: PaymentHash, recipient_onion: RecipientOnionFields,
/// # route_params: RouteParameters, retry: Retry
/// # channel_manager: T, invoice: &Bolt11Invoice, route_params_config: RouteParametersConfig,
/// # retry: Retry
/// # ) {
/// # let channel_manager = channel_manager.get_cm();
/// // let (payment_hash, recipient_onion, route_params) =
/// // payment::payment_parameters_from_invoice(&invoice);
/// let payment_id = PaymentId([42; 32]);
/// match channel_manager.send_payment(
/// payment_hash, recipient_onion, payment_id, route_params, retry
/// # let payment_id = PaymentId([42; 32]);
/// # let payment_hash = PaymentHash((*invoice.payment_hash()).to_byte_array());
/// match channel_manager.pay_for_bolt11_invoice(
/// invoice, payment_id, None, route_params_config, retry
/// ) {
/// Ok(()) => println!("Sending payment with hash {}", payment_hash),
/// Err(e) => println!("Failed sending payment with hash {}: {:?}", payment_hash, e),
@@ -2379,7 +2377,6 @@ where
/// [`create_bolt11_invoice`]: Self::create_bolt11_invoice
/// [`create_inbound_payment`]: Self::create_inbound_payment
/// [`create_inbound_payment_for_hash`]: Self::create_inbound_payment_for_hash
/// [`bolt11_payment`]: crate::ln::bolt11_payment
/// [`claim_funds`]: Self::claim_funds
/// [`send_payment`]: Self::send_payment
/// [`offers`]: crate::offers
@@ -4769,6 +4766,34 @@ where
self.pending_outbound_payments.test_set_payment_metadata(payment_id, new_payment_metadata);
}

/// Pays a [`Bolt11Invoice`] associated with the `payment_id`. See [`Self::send_payment`] for more info.
///
/// # Payment Id
/// The invoice's `payment_hash().0` serves as a reliable choice for the `payment_id`.
///
/// # Handling Invoice Amounts
/// Some invoices include a specific amount, while others require you to specify one.
/// - If the invoice **includes** an amount, user must not provide `amount_msats`.
/// - If the invoice **doesn't include** an amount, you'll need to specify `amount_msats`.
///
/// If these conditions aren’t met, the function will return `Bolt11PaymentError::InvalidAmount`.
///
/// # Custom Routing Parameters
/// Users can customize routing parameters via [`RouteParametersConfig`].
/// To use default settings, call the function with `RouteParametersConfig::default()`.
pub fn pay_for_bolt11_invoice(
&self, invoice: &Bolt11Invoice, payment_id: PaymentId, amount_msats: Option<u64>,
route_params_config: RouteParametersConfig, retry_strategy: Retry
) -> Result<(), Bolt11PaymentError> {
let best_block_height = self.best_block.read().unwrap().height;
let _persistence_guard = PersistenceNotifierGuard::notify_on_drop(self);
self.pending_outbound_payments
.pay_for_bolt11_invoice(invoice, payment_id, amount_msats, route_params_config, retry_strategy,
&self.router, self.list_usable_channels(), || self.compute_inflight_htlcs(),
&self.entropy_source, &self.node_signer, best_block_height, &self.logger,
&self.pending_events, |args| self.send_payment_along_path(args))
}

/// Pays the [`Bolt12Invoice`] associated with the `payment_id` encoded in its `payer_metadata`.
///
/// The invoice's `payer_metadata` is used to authenticate that the invoice was indeed requested
18 changes: 6 additions & 12 deletions lightning/src/ln/invoice_utils.rs
Original file line number Diff line number Diff line change
@@ -715,7 +715,7 @@ mod test {
use crate::ln::channelmanager::{Bolt11InvoiceParameters, PhantomRouteHints, MIN_FINAL_CLTV_EXPIRY_DELTA, PaymentId, RecipientOnionFields, Retry};
use crate::ln::functional_test_utils::*;
use crate::ln::msgs::{BaseMessageHandler, ChannelMessageHandler, MessageSendEvent};
use crate::routing::router::{PaymentParameters, RouteParameters};
use crate::routing::router::{PaymentParameters, RouteParameters, RouteParametersConfig};
use crate::util::test_utils;
use crate::util::config::UserConfig;
use std::collections::HashSet;
@@ -750,7 +750,7 @@ mod test {


#[test]
fn test_from_channelmanager() {
fn create_and_pay_for_bolt11_invoice() {
let chanmon_cfgs = create_chanmon_cfgs(2);
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]);
@@ -784,17 +784,11 @@ mod test {
assert_eq!(invoice.route_hints()[0].0[0].htlc_minimum_msat, chan.inbound_htlc_minimum_msat);
assert_eq!(invoice.route_hints()[0].0[0].htlc_maximum_msat, chan.inbound_htlc_maximum_msat);

let payment_params = PaymentParameters::from_node_id(invoice.recover_payee_pub_key(),
invoice.min_final_cltv_expiry_delta() as u32)
.with_bolt11_features(invoice.features().unwrap().clone()).unwrap()
.with_route_hints(invoice.route_hints()).unwrap();
let route_params = RouteParameters::from_payment_params_and_value(
payment_params, invoice.amount_milli_satoshis().unwrap());
let payment_event = {
let payment_hash = PaymentHash(invoice.payment_hash().to_byte_array());
nodes[0].node.send_payment(payment_hash,
RecipientOnionFields::secret_only(*invoice.payment_secret()),
PaymentId(payment_hash.0), route_params, Retry::Attempts(0)).unwrap();
nodes[0].node.pay_for_bolt11_invoice(
&invoice, PaymentId([42; 32]), None, RouteParametersConfig::default(),
Retry::Attempts(0)
).unwrap();
check_added_monitors(&nodes[0], 1);

let mut events = nodes[0].node.get_and_clear_pending_msg_events();
4 changes: 3 additions & 1 deletion lightning/src/ln/mod.rs
Original file line number Diff line number Diff line change
@@ -28,7 +28,6 @@ pub mod types;
// TODO: These modules were moved from lightning-invoice and need to be better integrated into this
// crate now:
pub mod invoice_utils;
pub mod bolt11_payment;

#[cfg(fuzzing)]
pub mod peer_channel_encryptor;
@@ -52,6 +51,9 @@ pub use onion_utils::create_payment_onion;
// without the node parameter being mut. This is incorrect, and thus newer rustcs will complain
// about an unnecessary mut. Thus, we silence the unused_mut warning in two test modules below.

#[cfg(test)]
#[allow(unused_mut)]
pub mod bolt11_payment_tests;
#[cfg(test)]
#[allow(unused_mut)]
mod blinded_payment_tests;
61 changes: 61 additions & 0 deletions lightning/src/ln/outbound_payment.rs
Original file line number Diff line number Diff line change
@@ -12,6 +12,7 @@
use bitcoin::hashes::Hash;
use bitcoin::hashes::sha256::Hash as Sha256;
use bitcoin::secp256k1::{self, Secp256k1, SecretKey};
use lightning_invoice::Bolt11Invoice;

use crate::blinded_path::{IntroductionNode, NodeIdLookUp};
use crate::events::{self, PaymentFailureReason};
@@ -570,6 +571,22 @@ pub(crate) enum PaymentSendFailure {
},
}

/// An error when attempting to pay a [`Bolt11Invoice`].
///
/// [`Bolt11Invoice`]: lightning_invoice::Bolt11Invoice
#[derive(Debug)]
pub enum Bolt11PaymentError {
/// Incorrect amount was provided to [`ChannelManager::pay_for_bolt11_invoice`].
/// This happens when an amount is specified when [`Bolt11Invoice`] already contains
/// an amount, or vice versa.
///
/// [`Bolt11Invoice`]: lightning_invoice::Bolt11Invoice
/// [`ChannelManager::pay_for_bolt11_invoice`]: crate::ln::channelmanager::ChannelManager::pay_for_bolt11_invoice
InvalidAmount,
/// The invoice was valid for the corresponding [`PaymentId`], but sending the payment failed.
SendingFailed(RetryableSendFailure),
}

/// An error when attempting to pay a [`Bolt12Invoice`].
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Bolt12PaymentError {
@@ -843,6 +860,50 @@ impl OutboundPayments {
.map(|()| payment_hash)
}

pub(super) fn pay_for_bolt11_invoice<R: Deref, ES: Deref, NS: Deref, IH, SP, L: Deref>(
&self, invoice: &Bolt11Invoice, payment_id: PaymentId,
amount_msats: Option<u64>,
route_params_config: RouteParametersConfig,
retry_strategy: Retry,
router: &R,
first_hops: Vec<ChannelDetails>, compute_inflight_htlcs: IH, entropy_source: &ES,
node_signer: &NS, best_block_height: u32, logger: &L,
pending_events: &Mutex<VecDeque<(events::Event, Option<EventCompletionAction>)>>, send_payment_along_path: SP,
) -> Result<(), Bolt11PaymentError>
where
R::Target: Router,
ES::Target: EntropySource,
NS::Target: NodeSigner,
L::Target: Logger,
IH: Fn() -> InFlightHtlcs,
SP: Fn(SendAlongPathArgs) -> Result<(), APIError>,
{
let payment_hash = PaymentHash((*invoice.payment_hash()).to_byte_array());

let amount = match (invoice.amount_milli_satoshis(), amount_msats) {
(Some(amt), None) | (None, Some(amt)) => amt,
(None, None) | (Some(_), Some(_)) => return Err(Bolt11PaymentError::InvalidAmount),
};

let mut recipient_onion = RecipientOnionFields::secret_only(*invoice.payment_secret());
recipient_onion.payment_metadata = invoice.payment_metadata().map(|v| v.clone());

let payment_params = PaymentParameters::from_bolt11_invoice(invoice)
.with_user_config_ignoring_fee_limit(route_params_config);

let mut route_params = RouteParameters::from_payment_params_and_value(payment_params, amount);

if let Some(max_fee_msat) = route_params_config.max_total_routing_fee_msat {
route_params.max_total_routing_fee_msat = Some(max_fee_msat);
}

self.send_payment_internal(payment_id, payment_hash, recipient_onion, None, retry_strategy, route_params,
router, first_hops, compute_inflight_htlcs,
entropy_source, node_signer, best_block_height, logger,
pending_events, send_payment_along_path
).map_err(|err| Bolt11PaymentError::SendingFailed(err))
}

pub(super) fn send_payment_for_bolt12_invoice<
R: Deref, ES: Deref, NS: Deref, NL: Deref, IH, SP, L: Deref
>(
22 changes: 22 additions & 0 deletions lightning/src/routing/router.rs
Original file line number Diff line number Diff line change
@@ -10,6 +10,7 @@
//! The router finds paths within a [`NetworkGraph`] for a payment.
use bitcoin::secp256k1::{PublicKey, Secp256k1, self};
use lightning_invoice::Bolt11Invoice;

use crate::blinded_path::{BlindedHop, Direction, IntroductionNode};
use crate::blinded_path::payment::{BlindedPaymentPath, ForwardTlvs, PaymentConstraints, PaymentForwardNode, PaymentRelay, ReceiveTlvs};
@@ -910,6 +911,27 @@ impl PaymentParameters {
.expect("PaymentParameters::from_node_id should always initialize the payee as unblinded")
}

/// Creates parameters for paying to a blinded payee from the provided invoice. Sets
/// [`Payee::Blinded::route_hints`], [`Payee::Blinded::features`], and
/// [`PaymentParameters::expiry_time`].
pub fn from_bolt11_invoice(invoice: &Bolt11Invoice) -> Self {
let mut payment_params = Self::from_node_id(
invoice.recover_payee_pub_key(),
invoice.min_final_cltv_expiry_delta() as u32,
)
.with_route_hints(invoice.route_hints())
.unwrap();

if let Some(expiry) = invoice.expires_at() {
payment_params = payment_params.with_expiry_time(expiry.as_secs());
}
if let Some(features) = invoice.features() {
payment_params = payment_params.with_bolt11_features(features.clone()).unwrap();
}

payment_params
}

/// Creates parameters for paying to a blinded payee from the provided invoice. Sets
/// [`Payee::Blinded::route_hints`], [`Payee::Blinded::features`], and
/// [`PaymentParameters::expiry_time`].