Skip to content

Provide Bolt12Invoice used for inbound payment #2929

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

Closed
wants to merge 5 commits into from
Closed
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
56 changes: 56 additions & 0 deletions lightning/src/events/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ use crate::ln::features::ChannelTypeFeatures;
use crate::ln::msgs;
use crate::ln::{ChannelId, PaymentPreimage, PaymentHash, PaymentSecret};
use crate::chain::transaction;
use crate::offers::invoice::Bolt12Invoice;
use crate::routing::gossip::NetworkUpdate;
use crate::util::errors::APIError;
use crate::util::ser::{BigSize, FixedLengthReader, Writeable, Writer, MaybeReadable, Readable, RequiredWrapper, UpgradableRequired, WithoutLength};
Expand Down Expand Up @@ -582,6 +583,43 @@ pub enum Event {
/// The `payment_id` to have been associated with payment for the requested invoice.
payment_id: PaymentId,
},
/// Indicates that a [`Bolt12Invoice`] was generated in response to an [`InvoiceRequest`] and is
/// being prepared to be sent via an [`OnionMessage`]. The event is provided only for invoices
/// corresponding to an [`Offer`], not for a [`Refund`]. For the latter, the invoice is returned
/// by [`ChannelManager::request_refund_payment`].
///
/// Note that this doesn't necessarily mean that the invoice was sent and -- once sent -- it may
/// never reach its destination because of the unreliable nature of onion messages. Any of the
/// following scenarios may occur.
/// - Dropped by a node along the path to the destination
/// - Dropped upon node restart prior to being sent
/// - Buffered waiting to be sent by [`PeerManager`]
/// - Buffered waiting for an [`Event::ConnectionNeeded`] to be handled and peer connected
/// - Dropped waiting too long for such a peer connection
/// - Dropped because the onion message buffer was full
/// - Dropped because the [`MessageRouter`] failed to find an [`OnionMessagePath`] to the
/// destination
///
/// Thus, this event is largely for informational purposes as the corresponding [`Offer`] and
/// [`InvoiceRequest`] fields are accessible from the invoice. In particular:
/// - [`Bolt12Invoice::metadata`] can help identify the corresponding [`Offer`]
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we make it a bit more explicit that this is the only way users can associate incoming payments with a previously-generated offer? I.e., the only way to know what was actually paid for is to handle this event and keep track of all sent-out payment hashes to be able to associate them with the offer based on metadata.

While this is probably fine for now, I really wonder if we eventually should take care of the tracking in LDK (or offer a utility for it at least), as every node receiving payments will want to know what a payment is for.

/// - A common [`Bolt12Invoice::payer_id`] indicates the payer sent multiple requests for
/// redundancy, though in that case the [`Bolt12Invoice::payment_hash`] used may be different.
///
/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
/// [`OnionMessage`]: crate::ln::msgs::OnionMessage
/// [`Offer`]: crate::offers::offer::Offer
/// [`Refund`]: crate::offers::refund::Refund
/// [`ChannelManager::request_refund_payment`]: crate::ln::channelmanager::ChannelManager::request_refund_payment
/// [`PeerManager`]: crate::ln::peer_handler::PeerManager
/// [`MessageRouter`]: crate::onion_message::messenger::MessageRouter
/// [`OnionMessagePath`]: crate::onion_message::messenger::OnionMessagePath
InvoiceGenerated {
/// An invoice that was generated in response to an [`InvoiceRequest`].
///
/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
invoice: Bolt12Invoice,
},
Copy link
Contributor

@tnull tnull Mar 12, 2024

Choose a reason for hiding this comment

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

I wonder if we should provide the preimage/payment secret also while we're here?

/// Indicates an outbound payment we made succeeded (i.e. it made it all the way to its target
/// and we got back the payment preimage for it).
///
Expand Down Expand Up @@ -1262,6 +1300,12 @@ impl Writeable for Event {
35u8.write(writer)?;
// Never write ConnectionNeeded events as buffered onion messages aren't serialized.
},
&Event::InvoiceGenerated { ref invoice } => {
37u8.write(writer)?;
write_tlv_fields!(writer, {
(0, invoice, required),
})
},
// Note that, going forward, all new events must only write data inside of
// `write_tlv_fields`. Versions 0.0.101+ will ignore odd-numbered events that write
// data via `write_tlv_fields`.
Expand Down Expand Up @@ -1668,6 +1712,18 @@ impl MaybeReadable for Event {
},
// Note that we do not write a length-prefixed TLV for ConnectionNeeded events.
35u8 => Ok(None),
37u8 => {
let f = || {
let mut invoice_bytes = WithoutLength(Vec::new());
Copy link
Contributor

Choose a reason for hiding this comment

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

Any reason we can't just implement Readable for Bolt12Invoice? Seems avoiding it forces users (namely LDK Node, heh.) to write the same custom deserializaiton code. Same goes for Offer, Refund, and Bolt11Invoice, fwiw.

Copy link
Contributor

Choose a reason for hiding this comment

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

serde would be great too

read_tlv_fields!(reader, {
(0, invoice_bytes, required),
});
let invoice = Bolt12Invoice::try_from(invoice_bytes.0)
.map_err(|_| msgs::DecodeError::InvalidValue)?;
Ok(Some(Event::InvoiceGenerated { invoice }))
};
f()
},
// Versions prior to 0.0.100 did not ignore odd types, instead returning InvalidValue.
// Version 0.0.100 failed to properly ignore odd types, possibly resulting in corrupt
// reads.
Expand Down
92 changes: 47 additions & 45 deletions lightning/src/ln/channelmanager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@ use crate::ln::wire::Encode;
use crate::offers::invoice::{BlindedPayInfo, Bolt12Invoice, DEFAULT_RELATIVE_EXPIRY, DerivedSigningPubkey, ExplicitSigningPubkey, InvoiceBuilder, UnsignedBolt12Invoice};
use crate::offers::invoice_error::InvoiceError;
use crate::offers::invoice_request::{DerivedPayerId, InvoiceRequestBuilder};
use crate::offers::merkle::SignError;
use crate::offers::offer::{Offer, OfferBuilder};
use crate::offers::parse::Bolt12SemanticError;
use crate::offers::refund::{Refund, RefundBuilder};
Expand Down Expand Up @@ -7907,7 +7906,7 @@ where
///
/// The resulting invoice uses a [`PaymentHash`] recognized by the [`ChannelManager`] and a
/// [`BlindedPath`] containing the [`PaymentSecret`] needed to reconstruct the corresponding
/// [`PaymentPreimage`].
/// [`PaymentPreimage`]. It is returned purely for informational purposes.
///
/// # Limitations
///
Expand All @@ -7922,7 +7921,9 @@ where
/// path for the invoice.
///
/// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice
pub fn request_refund_payment(&self, refund: &Refund) -> Result<(), Bolt12SemanticError> {
pub fn request_refund_payment(
&self, refund: &Refund
) -> Result<Bolt12Invoice, Bolt12SemanticError> {
let expanded_key = &self.inbound_payment_key;
let entropy = &*self.entropy_source;
let secp_ctx = &self.secp_ctx;
Expand Down Expand Up @@ -7955,7 +7956,7 @@ where
let mut pending_offers_messages = self.pending_offers_messages.lock().unwrap();
if refund.paths().is_empty() {
let message = new_pending_onion_message(
OffersMessage::Invoice(invoice),
OffersMessage::Invoice(invoice.clone()),
Destination::Node(refund.payer_id()),
Some(reply_path),
);
Expand All @@ -7971,7 +7972,7 @@ where
}
}

Ok(())
Ok(invoice)
},
Err(()) => Err(Bolt12SemanticError::InvalidAmount),
}
Expand Down Expand Up @@ -9451,7 +9452,7 @@ where
self.highest_seen_timestamp.load(Ordering::Acquire) as u64
);

if invoice_request.keys.is_some() {
let response = if invoice_request.keys.is_some() {
#[cfg(feature = "std")]
let builder = invoice_request.respond_using_derived_keys(
payment_paths, payment_hash
Expand All @@ -9460,60 +9461,61 @@ where
let builder = invoice_request.respond_using_derived_keys_no_std(
payment_paths, payment_hash, created_at
);
let builder: Result<InvoiceBuilder<DerivedSigningPubkey>, _> =
builder.map(|b| b.into());
match builder.and_then(|b| b.allow_mpp().build_and_sign(secp_ctx)) {
Ok(invoice) => Some(OffersMessage::Invoice(invoice)),
Err(error) => Some(OffersMessage::InvoiceError(error.into())),
}
builder
.map(InvoiceBuilder::<DerivedSigningPubkey>::from)
.and_then(|builder| builder.allow_mpp().build_and_sign(secp_ctx))
.map_err(InvoiceError::from)
} else {
#[cfg(feature = "std")]
let builder = invoice_request.respond_with(payment_paths, payment_hash);
#[cfg(not(feature = "std"))]
let builder = invoice_request.respond_with_no_std(
payment_paths, payment_hash, created_at
);
let builder: Result<InvoiceBuilder<ExplicitSigningPubkey>, _> =
builder.map(|b| b.into());
let response = builder.and_then(|builder| builder.allow_mpp().build())
.map_err(|e| OffersMessage::InvoiceError(e.into()))
builder
.map(InvoiceBuilder::<ExplicitSigningPubkey>::from)
.and_then(|builder| builder.allow_mpp().build())
.map_err(InvoiceError::from)
.and_then(|invoice| {
#[cfg(c_bindings)]
let mut invoice = invoice;
match invoice.sign(|invoice: &UnsignedBolt12Invoice|
self.node_signer.sign_bolt12_invoice(invoice)
) {
Ok(invoice) => Ok(OffersMessage::Invoice(invoice)),
Err(SignError::Signing) => Err(OffersMessage::InvoiceError(
InvoiceError::from_string("Failed signing invoice".to_string())
)),
Err(SignError::Verification(_)) => Err(OffersMessage::InvoiceError(
InvoiceError::from_string("Failed invoice signature verification".to_string())
)),
}
});
match response {
Ok(invoice) => Some(invoice),
Err(error) => Some(error),
}
invoice
.sign(|invoice: &UnsignedBolt12Invoice|
self.node_signer.sign_bolt12_invoice(invoice)
)
.map_err(InvoiceError::from)
})
};

match response {
Ok(invoice) => {
let event = Event::InvoiceGenerated { invoice: invoice.clone() };
self.pending_events.lock().unwrap().push_back((event, None));
Some(OffersMessage::Invoice(invoice))
},
Err(error) => Some(OffersMessage::InvoiceError(error.into())),
}
},
OffersMessage::Invoice(invoice) => {
match invoice.verify(expanded_key, secp_ctx) {
Err(()) => {
Some(OffersMessage::InvoiceError(InvoiceError::from_string("Unrecognized invoice".to_owned())))
},
Ok(_) if invoice.invoice_features().requires_unknown_bits_from(&self.bolt12_invoice_features()) => {
Some(OffersMessage::InvoiceError(Bolt12SemanticError::UnknownRequiredFeatures.into()))
},
Ok(payment_id) => {
if let Err(e) = self.send_payment_for_bolt12_invoice(&invoice, payment_id) {
log_trace!(self.logger, "Failed paying invoice: {:?}", e);
Some(OffersMessage::InvoiceError(InvoiceError::from_string(format!("{:?}", e))))
let response = invoice
.verify(expanded_key, secp_ctx)
.map_err(|()| InvoiceError::from_string("Unrecognized invoice".to_owned()))
.and_then(|payment_id| {
let features = self.bolt12_invoice_features();
if invoice.invoice_features().requires_unknown_bits_from(&features) {
Err(InvoiceError::from(Bolt12SemanticError::UnknownRequiredFeatures))
} else {
None
self.send_payment_for_bolt12_invoice(&invoice, payment_id)
.map_err(|e| {
log_trace!(self.logger, "Failed paying invoice: {:?}", e);
InvoiceError::from_string(format!("{:?}", e))
})
}
},
});

match response {
Ok(()) => None,
Err(e) => Some(OffersMessage::InvoiceError(e)),
}
},
OffersMessage::InvoiceError(invoice_error) => {
Expand Down
36 changes: 33 additions & 3 deletions lightning/src/ln/offers_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,24 @@ fn extract_invoice_error<'a, 'b, 'c>(
}
}

fn expect_invoice_generated_event<'a, 'b, 'c, 'd>(
node: &'a Node<'b, 'c, 'd>, expected_invoice: &Bolt12Invoice
) {
use crate::io::Cursor;
use crate::util::ser::MaybeReadable;
use crate::util::ser::Writeable;

let event = get_event!(node, Event::InvoiceGenerated);
match &event {
Event::InvoiceGenerated { invoice } => assert_eq!(invoice, expected_invoice),
_ => panic!(),
}

let mut bytes = Vec::new();
event.write(&mut bytes).unwrap();
assert_eq!(Some(event), MaybeReadable::read(&mut Cursor::new(&bytes)).unwrap());
}

/// Checks that blinded paths without Tor-only nodes are preferred when constructing an offer.
#[test]
fn prefers_non_tor_nodes_in_blinded_paths() {
Expand Down Expand Up @@ -403,6 +421,8 @@ fn creates_and_pays_for_offer_using_two_hop_blinded_path() {
david.onion_messenger.handle_onion_message(&charlie_id, &onion_message);

let invoice = extract_invoice(david, &onion_message);
expect_invoice_generated_event(alice, &invoice);

assert_eq!(invoice.amount_msats(), 10_000_000);
assert_ne!(invoice.signing_pubkey(), alice_id);
assert!(!invoice.payment_paths().is_empty());
Expand Down Expand Up @@ -472,7 +492,7 @@ fn creates_and_pays_for_refund_using_two_hop_blinded_path() {
}
expect_recent_payment!(david, RecentPaymentDetails::AwaitingInvoice, payment_id);

alice.node.request_refund_payment(&refund).unwrap();
let expected_invoice = alice.node.request_refund_payment(&refund).unwrap();

connect_peers(alice, charlie);

Expand All @@ -483,6 +503,8 @@ fn creates_and_pays_for_refund_using_two_hop_blinded_path() {
david.onion_messenger.handle_onion_message(&charlie_id, &onion_message);

let invoice = extract_invoice(david, &onion_message);
assert_eq!(invoice, expected_invoice);

assert_eq!(invoice.amount_msats(), 10_000_000);
assert_ne!(invoice.signing_pubkey(), alice_id);
assert!(!invoice.payment_paths().is_empty());
Expand Down Expand Up @@ -540,6 +562,8 @@ fn creates_and_pays_for_offer_using_one_hop_blinded_path() {
bob.onion_messenger.handle_onion_message(&alice_id, &onion_message);

let invoice = extract_invoice(bob, &onion_message);
expect_invoice_generated_event(alice, &invoice);

assert_eq!(invoice.amount_msats(), 10_000_000);
assert_ne!(invoice.signing_pubkey(), alice_id);
assert!(!invoice.payment_paths().is_empty());
Expand Down Expand Up @@ -588,12 +612,14 @@ fn creates_and_pays_for_refund_using_one_hop_blinded_path() {
}
expect_recent_payment!(bob, RecentPaymentDetails::AwaitingInvoice, payment_id);

alice.node.request_refund_payment(&refund).unwrap();
let expected_invoice = alice.node.request_refund_payment(&refund).unwrap();

let onion_message = alice.onion_messenger.next_onion_message_for_peer(bob_id).unwrap();
bob.onion_messenger.handle_onion_message(&alice_id, &onion_message);

let invoice = extract_invoice(bob, &onion_message);
assert_eq!(invoice, expected_invoice);

assert_eq!(invoice.amount_msats(), 10_000_000);
assert_ne!(invoice.signing_pubkey(), alice_id);
assert!(!invoice.payment_paths().is_empty());
Expand Down Expand Up @@ -644,6 +670,8 @@ fn pays_for_offer_without_blinded_paths() {
bob.onion_messenger.handle_onion_message(&alice_id, &onion_message);

let invoice = extract_invoice(bob, &onion_message);
expect_invoice_generated_event(alice, &invoice);

route_bolt12_payment(bob, &[alice], &invoice);
expect_recent_payment!(bob, RecentPaymentDetails::Pending, payment_id);

Expand Down Expand Up @@ -680,12 +708,14 @@ fn pays_for_refund_without_blinded_paths() {
assert!(refund.paths().is_empty());
expect_recent_payment!(bob, RecentPaymentDetails::AwaitingInvoice, payment_id);

alice.node.request_refund_payment(&refund).unwrap();
let expected_invoice = alice.node.request_refund_payment(&refund).unwrap();

let onion_message = alice.onion_messenger.next_onion_message_for_peer(bob_id).unwrap();
bob.onion_messenger.handle_onion_message(&alice_id, &onion_message);

let invoice = extract_invoice(bob, &onion_message);
assert_eq!(invoice, expected_invoice);

route_bolt12_payment(bob, &[alice], &invoice);
expect_recent_payment!(bob, RecentPaymentDetails::Pending, payment_id);

Expand Down
Loading