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 numberDiff line numberDiff 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 DownExpand 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
Copy Markdown
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,
},

@tnulltnullMar 12, 2024

Copy link
Copy Markdown
Contributor

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 DownExpand 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 DownExpand 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
Copy Markdown
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
Copy Markdown
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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
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 numberDiff line numberDiff 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 DownExpand 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
Copy Markdown
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,
},

@tnulltnullMar 12, 2024

Copy link
Copy Markdown
Contributor

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 DownExpand 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 DownExpand 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
Copy Markdown
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
Copy Markdown
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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
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 numberDiff line numberDiff 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 DownExpand 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
Copy Markdown
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,
},

@tnulltnullMar 12, 2024

Copy link
Copy Markdown
Contributor

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 DownExpand 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 DownExpand 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
Copy Markdown
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
Copy Markdown
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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
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 numberDiff line numberDiff 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 DownExpand 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
Copy Markdown
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,
},

@tnulltnullMar 12, 2024

Copy link
Copy Markdown
Contributor

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 DownExpand 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 DownExpand 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
Copy Markdown
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
Copy Markdown
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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
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 numberDiff line numberDiff 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 DownExpand 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
Copy Markdown
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,
},

@tnulltnullMar 12, 2024

Copy link
Copy Markdown
Contributor

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 DownExpand 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 DownExpand 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
Copy Markdown
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
Copy Markdown
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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
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 numberDiff line numberDiff 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 DownExpand 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
Copy Markdown
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,
},

@tnulltnullMar 12, 2024

Copy link
Copy Markdown
Contributor

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 DownExpand 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 DownExpand 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
Copy Markdown
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
Copy Markdown
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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
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 numberDiff line numberDiff 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 DownExpand 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
Copy Markdown
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,
},

@tnulltnullMar 12, 2024

Copy link
Copy Markdown
Contributor

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 DownExpand 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 DownExpand 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
Copy Markdown
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
Copy Markdown
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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
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 numberDiff line numberDiff 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 DownExpand 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
Copy Markdown
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,
},

@tnulltnullMar 12, 2024

Copy link
Copy Markdown
Contributor

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 DownExpand 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 DownExpand 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
Copy Markdown
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
Copy Markdown
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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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