From 4a0ba424c3cf9bffbf4dd1b45eafb40025055f77 Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Wed, 5 Feb 2025 16:33:27 -0800 Subject: [PATCH 1/3] Un-cfg-gate StaticInvoices We need to include static invoices in the public API as part of the onion messages we're adding for static invoice server support. Utilities to create these static invoices and other parts of the async receive API will remain cfg-gated for now. Generally, we can't totally avoid exposing half baked async receive support in the public API because OnionMessenger is parameterized by an async payments message handler, which can't be cfg-gated easily. Signed-off-by: Vincenzo Palazzo --- lightning/src/offers/invoice_request.rs | 7 ++++++- lightning/src/offers/mod.rs | 1 - lightning/src/offers/offer.rs | 1 - lightning/src/offers/static_invoice.rs | 1 + 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index eecb68e0826..cdd9cff2177 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -858,7 +858,7 @@ impl InvoiceRequest { ); invoice_request_verify_method!(self, Self); - #[cfg(async_payments)] + #[allow(unused)] // TODO: remove this once we remove the `async_payments` cfg flag pub(super) fn bytes(&self) -> &Vec { &self.bytes } @@ -874,6 +874,11 @@ impl InvoiceRequest { InvoiceWithExplicitSigningPubkeyBuilder ); invoice_request_verify_method!(self, &Self); + + #[allow(unused)] // TODO: remove this once we remove the `async_payments` cfg flag + pub(super) fn bytes(&self) -> &Vec { + &self.bytes + } } impl InvoiceRequest { diff --git a/lightning/src/offers/mod.rs b/lightning/src/offers/mod.rs index e4fe7d789db..49a95b96f86 100644 --- a/lightning/src/offers/mod.rs +++ b/lightning/src/offers/mod.rs @@ -25,7 +25,6 @@ pub mod parse; mod payer; pub mod refund; pub(crate) mod signer; -#[cfg(async_payments)] pub mod static_invoice; #[cfg(test)] pub(crate) mod test_utils; diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs index d4330df3223..f84b39f9a4e 100644 --- a/lightning/src/offers/offer.rs +++ b/lightning/src/offers/offer.rs @@ -743,7 +743,6 @@ impl Offer { .chain(TlvStream::new(bytes).range(EXPERIMENTAL_OFFER_TYPES)) } - #[cfg(async_payments)] pub(super) fn verify( &self, nonce: Nonce, key: &ExpandedKey, secp_ctx: &Secp256k1, ) -> Result<(OfferId, Option), ()> { diff --git a/lightning/src/offers/static_invoice.rs b/lightning/src/offers/static_invoice.rs index 0ebf33a23dc..e221a11913a 100644 --- a/lightning/src/offers/static_invoice.rs +++ b/lightning/src/offers/static_invoice.rs @@ -379,6 +379,7 @@ impl StaticInvoice { self.signature } + #[allow(unused)] // TODO: remove this once we remove the `async_payments` cfg flag pub(crate) fn from_same_offer(&self, invreq: &InvoiceRequest) -> bool { let invoice_offer_tlv_stream = Offer::tlv_stream_iter(&self.bytes).map(|tlv_record| tlv_record.record_bytes); From af9ed81877f0f4a5f88d378562a3906a86f4150d Mon Sep 17 00:00:00 2001 From: Vincenzo Palazzo Date: Mon, 17 Mar 2025 22:13:02 +0100 Subject: [PATCH 2/3] ser: support LengthLimitedRead in impl_writeable_tlv_based_enum When introducing the LengthLimitedRead trait in [1], for some serialization macros we need this change to avoid that we have the following compilation error error[E0277]: the trait bound `Bolt12Invoice: Readable` is not satisfied --> lightning/src/util/ser_macros.rs:1243:35 | 1243 | Ok($st::$tuple_variant_name($crate::util::ser::Readable::read(reader)?)) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Readable` is not implemented for `Bolt12Invoice` | ::: lightning/src/events/mod.rs:2463:1 | 2463 | / impl_writeable_tlv_based_enum_legacy!(PaidInvoice, 2464 | | ; 2465 | | (0, Bolt12Invoice), 2466 | | (2, StaticInvoice) 2467 | | ); | |_- in this macro invocation | = help: the following other types implement trait `Readable`: () (A, B) (A, B, C) (A, B, C, D) (A, B, C, D, E) (A, B, C, D, E, F) (A, B, C, D, E, F, G) AnnouncementSigsState and 200 others = note: this error originates in the macro `impl_writeable_tlv_based_enum_legacy` (in Nightly builds, run with -Z macro-backtrace for more info) This changes are needed since [1] is landed on mainline. [1] https://github.com/lightningdevkit/rust-lightning/pull/3640 Co-Developed-by: @valentinewallace Suggested-by: @valentinewallace Signed-off-by: Vincenzo Palazzo --- lightning/src/util/ser_macros.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lightning/src/util/ser_macros.rs b/lightning/src/util/ser_macros.rs index 8c0e4bd3d10..c3cf2044deb 100644 --- a/lightning/src/util/ser_macros.rs +++ b/lightning/src/util/ser_macros.rs @@ -1225,7 +1225,7 @@ macro_rules! impl_writeable_tlv_based_enum { $($tuple_variant_id => { let length: $crate::util::ser::BigSize = $crate::util::ser::Readable::read(reader)?; let mut s = $crate::util::ser::FixedLengthReader::new(reader, length.0); - let res = $crate::util::ser::Readable::read(&mut s)?; + let res = $crate::util::ser::LengthReadable::read_from_fixed_length_buffer(&mut s)?; if s.bytes_remain() { s.eat_remaining()?; // Return ShortRead if there's actually not enough bytes return Err($crate::ln::msgs::DecodeError::InvalidValue); From 57772d32d75258ce09029c61d2da4a15c00d9899 Mon Sep 17 00:00:00 2001 From: Vincenzo Palazzo Date: Fri, 7 Feb 2025 23:17:10 +0100 Subject: [PATCH 3/3] add the bolt12 invoice to the PaymentSend event This commit make two things possible: 1. make persistent BOLT12 invoice through PendingOutboundPayment This commit prepares the code to pass down the BOLT12 invoice inside the `PaymentSent` event. To achieve this, the `bolt12` field has been added to the `PendingOutboundPayment::Retryable` enum, allowing it to be attached to the `PaymentSent` event when the payment is completed. 2. To enable proof of payment, we need to share the bolt12 invoice with the library user. This is already possible if we `manually_handle_bolt12_invoices`, but this approach requires a significant amount of work from the user. This commit adds the bolt12 invoice to the PaymentSend event when the payment is completed. This allows the user to always have the option to perform proof of payment. We also Inject the static invoice inside the PendingOutboundPayment::StaticInvoiceReceived to allow to use the static invoice inside the PendingOutboundPayment::PaymentReceived. Link: https://github.com/lightningdevkit/rust-lightning/issues/3344 Signed-off-by: Vincenzo Palazzo --- lightning/src/events/mod.rs | 37 ++++++++++- lightning/src/ln/async_payments_tests.rs | 9 ++- lightning/src/ln/functional_test_utils.rs | 26 +++++--- lightning/src/ln/offers_tests.rs | 30 +++++---- lightning/src/ln/outbound_payment.rs | 79 ++++++++++++++--------- lightning/src/offers/invoice_macros.rs | 2 +- lightning/src/offers/static_invoice.rs | 18 +++++- lightning/src/offers/test_utils.rs | 45 +++++++++++++ 8 files changed, 186 insertions(+), 60 deletions(-) diff --git a/lightning/src/events/mod.rs b/lightning/src/events/mod.rs index a0f26bfbac0..8d28c9b4191 100644 --- a/lightning/src/events/mod.rs +++ b/lightning/src/events/mod.rs @@ -23,11 +23,12 @@ use crate::blinded_path::payment::{Bolt12OfferContext, Bolt12RefundContext, Paym use crate::chain::transaction; use crate::ln::channelmanager::{InterceptId, PaymentId, RecipientOnionFields}; use crate::ln::channel::FUNDING_CONF_DEADLINE_BLOCKS; +use crate::offers::invoice::Bolt12Invoice; +use crate::offers::static_invoice::StaticInvoice; use crate::types::features::ChannelTypeFeatures; use crate::ln::msgs; use crate::ln::types::ChannelId; use crate::types::payment::{PaymentPreimage, PaymentHash, PaymentSecret}; -use crate::offers::invoice::Bolt12Invoice; use crate::onion_message::messenger::Responder; use crate::routing::gossip::NetworkUpdate; use crate::routing::router::{BlindedTail, Path, RouteHop, RouteParameters}; @@ -949,6 +950,18 @@ pub enum Event { /// /// [`Route::get_total_fees`]: crate::routing::router::Route::get_total_fees fee_paid_msat: Option, + /// The BOLT 12 invoice that was paid. `None` if the payment was a non BOLT 12 payment. + /// + /// The BOLT 12 invoice is useful for proof of payment because it contains the + /// payment hash. A third party can verify that the payment was made by + /// showing the invoice and confirming that the payment hash matches + /// the hash of the payment preimage. + /// + /// However, the [`PaidBolt12Invoice`] can also be of type [`StaticInvoice`], which + /// is a special [`Bolt12Invoice`] where proof of payment is not possible. + /// + /// [`StaticInvoice`]: crate::offers::static_invoice::StaticInvoice + bolt12_invoice: Option, }, /// Indicates an outbound payment failed. Individual [`Event::PaymentPathFailed`] events /// provide failure information for each path attempt in the payment, including retries. @@ -1556,7 +1569,7 @@ impl Writeable for Event { (13, payment_id, option), }); }, - &Event::PaymentSent { ref payment_id, ref payment_preimage, ref payment_hash, ref amount_msat, ref fee_paid_msat } => { + &Event::PaymentSent { ref payment_id, ref payment_preimage, ref payment_hash, ref amount_msat, ref fee_paid_msat, ref bolt12_invoice } => { 2u8.write(writer)?; write_tlv_fields!(writer, { (0, payment_preimage, required), @@ -1564,6 +1577,7 @@ impl Writeable for Event { (3, payment_id, option), (5, fee_paid_msat, option), (7, amount_msat, option), + (9, bolt12_invoice, option), }); }, &Event::PaymentPathFailed { @@ -1898,12 +1912,14 @@ impl MaybeReadable for Event { let mut payment_id = None; let mut amount_msat = None; let mut fee_paid_msat = None; + let mut bolt12_invoice = None; read_tlv_fields!(reader, { (0, payment_preimage, required), (1, payment_hash, option), (3, payment_id, option), (5, fee_paid_msat, option), (7, amount_msat, option), + (9, bolt12_invoice, option), }); if payment_hash.is_none() { payment_hash = Some(PaymentHash(Sha256::hash(&payment_preimage.0[..]).to_byte_array())); @@ -1914,6 +1930,7 @@ impl MaybeReadable for Event { payment_hash: payment_hash.unwrap(), amount_msat, fee_paid_msat, + bolt12_invoice, })) }; f() @@ -2438,3 +2455,19 @@ impl EventHandler for Arc { self.deref().handle_event(event) } } + +/// The BOLT 12 invoice that was paid, surfaced in [`Event::PaymentSent::bolt12_invoice`]. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum PaidBolt12Invoice { + /// The BOLT 12 invoice specified by the BOLT 12 specification, + /// allowing the user to perform proof of payment. + Bolt12Invoice(Bolt12Invoice), + /// The Static invoice, used in the async payment specification update proposal, + /// where the user cannot perform proof of payment. + StaticInvoice(StaticInvoice), + } + +impl_writeable_tlv_based_enum!(PaidBolt12Invoice, + {0, Bolt12Invoice} => (), + {2, StaticInvoice} => (), +); diff --git a/lightning/src/ln/async_payments_tests.rs b/lightning/src/ln/async_payments_tests.rs index b888b9ceb5c..5e0c7efcc17 100644 --- a/lightning/src/ln/async_payments_tests.rs +++ b/lightning/src/ln/async_payments_tests.rs @@ -11,7 +11,7 @@ use crate::blinded_path::message::{MessageContext, OffersContext}; use crate::blinded_path::payment::PaymentContext; use crate::blinded_path::payment::{AsyncBolt12OfferContext, BlindedPaymentTlvs}; use crate::chain::channelmonitor::{HTLC_FAIL_BACK_BUFFER, LATENCY_GRACE_PERIOD_BLOCKS}; -use crate::events::{Event, HTLCDestination, PaymentFailureReason}; +use crate::events::{Event, HTLCDestination, PaidBolt12Invoice, PaymentFailureReason}; use crate::ln::blinded_payment_tests::{fail_blinded_htlc_backwards, get_blinded_route_parameters}; use crate::ln::channelmanager::{PaymentId, RecipientOnionFields}; use crate::ln::functional_test_utils::*; @@ -420,7 +420,7 @@ fn async_receive_flow_success() { .pay_for_offer(&offer, None, Some(amt_msat), None, payment_id, Retry::Attempts(0), params) .unwrap(); let release_held_htlc_om = - pass_async_payments_oms(static_invoice, &nodes[0], &nodes[1], &nodes[2]).1; + pass_async_payments_oms(static_invoice.clone(), &nodes[0], &nodes[1], &nodes[2]).1; nodes[0] .onion_messenger .handle_onion_message(nodes[2].node.get_our_node_id(), &release_held_htlc_om); @@ -441,7 +441,10 @@ fn async_receive_flow_success() { let args = PassAlongPathArgs::new(&nodes[0], route[0], amt_msat, payment_hash, ev) .with_payment_preimage(keysend_preimage); do_pass_along_path(args); - claim_payment_along_route(ClaimAlongRouteArgs::new(&nodes[0], route, keysend_preimage)); + let res = + claim_payment_along_route(ClaimAlongRouteArgs::new(&nodes[0], route, keysend_preimage)); + assert!(res.is_some()); + assert_eq!(res, Some(PaidBolt12Invoice::StaticInvoice(static_invoice))); } #[cfg_attr(feature = "std", ignore)] diff --git a/lightning/src/ln/functional_test_utils.rs b/lightning/src/ln/functional_test_utils.rs index 56cd64a5567..a9be7ee3a57 100644 --- a/lightning/src/ln/functional_test_utils.rs +++ b/lightning/src/ln/functional_test_utils.rs @@ -13,7 +13,7 @@ use crate::chain::{BestBlock, ChannelMonitorUpdateStatus, Confirm, Listen, Watch, chainmonitor::Persist}; use crate::chain::channelmonitor::ChannelMonitor; use crate::chain::transaction::OutPoint; -use crate::events::{ClaimedHTLC, ClosureReason, Event, HTLCDestination, PathFailure, PaymentPurpose, PaymentFailureReason}; +use crate::events::{ClaimedHTLC, ClosureReason, Event, HTLCDestination, PaidBolt12Invoice, PathFailure, PaymentFailureReason, PaymentPurpose}; use crate::events::bump_transaction::{BumpTransactionEvent, BumpTransactionEventHandler, Wallet, WalletSource}; use crate::ln::types::ChannelId; use crate::types::payment::{PaymentPreimage, PaymentHash, PaymentSecret}; @@ -2294,7 +2294,7 @@ macro_rules! expect_payment_claimed { pub fn expect_payment_sent>(node: &H, expected_payment_preimage: PaymentPreimage, expected_fee_msat_opt: Option>, expect_per_path_claims: bool, expect_post_ev_mon_update: bool, -) { +) -> Option { let events = node.node().get_and_clear_pending_events(); let expected_payment_hash = PaymentHash( bitcoin::hashes::sha256::Hash::hash(&expected_payment_preimage.0).to_byte_array()); @@ -2306,8 +2306,10 @@ pub fn expect_payment_sent>(node: &H, if expect_post_ev_mon_update { check_added_monitors(node, 1); } + // We return the invoice because some test may want to check the invoice details. + let invoice; let expected_payment_id = match events[0] { - Event::PaymentSent { ref payment_id, ref payment_preimage, ref payment_hash, ref amount_msat, ref fee_paid_msat } => { + Event::PaymentSent { ref payment_id, ref payment_preimage, ref payment_hash, ref amount_msat, ref fee_paid_msat, ref bolt12_invoice } => { assert_eq!(expected_payment_preimage, *payment_preimage); assert_eq!(expected_payment_hash, *payment_hash); assert!(amount_msat.is_some()); @@ -2316,6 +2318,7 @@ pub fn expect_payment_sent>(node: &H, } else { assert!(fee_paid_msat.is_some()); } + invoice = bolt12_invoice.clone(); payment_id.unwrap() }, _ => panic!("Unexpected event"), @@ -2331,19 +2334,20 @@ pub fn expect_payment_sent>(node: &H, } } } + invoice } #[macro_export] macro_rules! expect_payment_sent { ($node: expr, $expected_payment_preimage: expr) => { - $crate::expect_payment_sent!($node, $expected_payment_preimage, None::, true); + $crate::expect_payment_sent!($node, $expected_payment_preimage, None::, true) }; ($node: expr, $expected_payment_preimage: expr, $expected_fee_msat_opt: expr) => { - $crate::expect_payment_sent!($node, $expected_payment_preimage, $expected_fee_msat_opt, true); + $crate::expect_payment_sent!($node, $expected_payment_preimage, $expected_fee_msat_opt, true) }; ($node: expr, $expected_payment_preimage: expr, $expected_fee_msat_opt: expr, $expect_paths: expr) => { $crate::ln::functional_test_utils::expect_payment_sent(&$node, $expected_payment_preimage, - $expected_fee_msat_opt.map(|o| Some(o)), $expect_paths, true); + $expected_fee_msat_opt.map(|o| Some(o)), $expect_paths, true) } } @@ -3106,20 +3110,22 @@ pub fn pass_claimed_payment_along_route(args: ClaimAlongRouteArgs) -> u64 { expected_total_fee_msat } -pub fn claim_payment_along_route(args: ClaimAlongRouteArgs) { +pub fn claim_payment_along_route(args: ClaimAlongRouteArgs) -> Option { let origin_node = args.origin_node; let payment_preimage = args.payment_preimage; let skip_last = args.skip_last; let expected_total_fee_msat = do_claim_payment_along_route(args); if !skip_last { - expect_payment_sent!(origin_node, payment_preimage, Some(expected_total_fee_msat)); + expect_payment_sent!(origin_node, payment_preimage, Some(expected_total_fee_msat)) + } else { + None } } -pub fn claim_payment<'a, 'b, 'c>(origin_node: &Node<'a, 'b, 'c>, expected_route: &[&Node<'a, 'b, 'c>], our_payment_preimage: PaymentPreimage) { +pub fn claim_payment<'a, 'b, 'c>(origin_node: &Node<'a, 'b, 'c>, expected_route: &[&Node<'a, 'b, 'c>], our_payment_preimage: PaymentPreimage) -> Option { claim_payment_along_route( ClaimAlongRouteArgs::new(origin_node, &[expected_route], our_payment_preimage) - ); + ) } pub const TEST_FINAL_CLTV: u32 = 70; diff --git a/lightning/src/ln/offers_tests.rs b/lightning/src/ln/offers_tests.rs index c8419a14239..78cd40e9e34 100644 --- a/lightning/src/ln/offers_tests.rs +++ b/lightning/src/ln/offers_tests.rs @@ -47,7 +47,7 @@ use crate::blinded_path::IntroductionNode; use crate::blinded_path::message::BlindedMessagePath; use crate::blinded_path::payment::{Bolt12OfferContext, Bolt12RefundContext, PaymentContext}; use crate::blinded_path::message::OffersContext; -use crate::events::{ClosureReason, Event, HTLCDestination, PaymentFailureReason, PaymentPurpose}; +use crate::events::{ClosureReason, Event, HTLCDestination, PaidBolt12Invoice, PaymentFailureReason, PaymentPurpose}; use crate::ln::channelmanager::{Bolt12PaymentError, MAX_SHORT_LIVED_RELATIVE_EXPIRY, PaymentId, RecentPaymentDetails, RecipientOnionFields, Retry, self}; use crate::types::features::Bolt12InvoiceFeatures; use crate::ln::functional_test_utils::*; @@ -167,7 +167,7 @@ fn route_bolt12_payment<'a, 'b, 'c>( } fn claim_bolt12_payment<'a, 'b, 'c>( - node: &Node<'a, 'b, 'c>, path: &[&Node<'a, 'b, 'c>], expected_payment_context: PaymentContext + node: &Node<'a, 'b, 'c>, path: &[&Node<'a, 'b, 'c>], expected_payment_context: PaymentContext, invoice: &Bolt12Invoice ) { let recipient = &path[path.len() - 1]; let payment_purpose = match get_event!(recipient, Event::PaymentClaimable) { @@ -187,7 +187,11 @@ fn claim_bolt12_payment<'a, 'b, 'c>( }, _ => panic!("Unexpected payment purpose: {:?}", payment_purpose), } - claim_payment(node, path, payment_preimage); + if let Some(inv) = claim_payment(node, path, payment_preimage) { + assert_eq!(inv, PaidBolt12Invoice::Bolt12Invoice(invoice.to_owned())); + } else { + panic!("Expected PaidInvoice::Bolt12Invoice"); + }; } fn extract_offer_nonce<'a, 'b, 'c>(node: &Node<'a, 'b, 'c>, message: &OnionMessage) -> Nonce { @@ -591,7 +595,7 @@ fn creates_and_pays_for_offer_using_two_hop_blinded_path() { route_bolt12_payment(david, &[charlie, bob, alice], &invoice); expect_recent_payment!(david, RecentPaymentDetails::Pending, payment_id); - claim_bolt12_payment(david, &[charlie, bob, alice], payment_context); + claim_bolt12_payment(david, &[charlie, bob, alice], payment_context, &invoice); expect_recent_payment!(david, RecentPaymentDetails::Fulfilled, payment_id); } @@ -674,7 +678,7 @@ fn creates_and_pays_for_refund_using_two_hop_blinded_path() { route_bolt12_payment(david, &[charlie, bob, alice], &invoice); expect_recent_payment!(david, RecentPaymentDetails::Pending, payment_id); - claim_bolt12_payment(david, &[charlie, bob, alice], payment_context); + claim_bolt12_payment(david, &[charlie, bob, alice], payment_context, &invoice); expect_recent_payment!(david, RecentPaymentDetails::Fulfilled, payment_id); } @@ -741,7 +745,7 @@ fn creates_and_pays_for_offer_using_one_hop_blinded_path() { route_bolt12_payment(bob, &[alice], &invoice); expect_recent_payment!(bob, RecentPaymentDetails::Pending, payment_id); - claim_bolt12_payment(bob, &[alice], payment_context); + claim_bolt12_payment(bob, &[alice], payment_context, &invoice); expect_recent_payment!(bob, RecentPaymentDetails::Fulfilled, payment_id); } @@ -797,7 +801,7 @@ fn creates_and_pays_for_refund_using_one_hop_blinded_path() { route_bolt12_payment(bob, &[alice], &invoice); expect_recent_payment!(bob, RecentPaymentDetails::Pending, payment_id); - claim_bolt12_payment(bob, &[alice], payment_context); + claim_bolt12_payment(bob, &[alice], payment_context, &invoice); expect_recent_payment!(bob, RecentPaymentDetails::Fulfilled, payment_id); } @@ -851,7 +855,7 @@ fn pays_for_offer_without_blinded_paths() { route_bolt12_payment(bob, &[alice], &invoice); expect_recent_payment!(bob, RecentPaymentDetails::Pending, payment_id); - claim_bolt12_payment(bob, &[alice], payment_context); + claim_bolt12_payment(bob, &[alice], payment_context, &invoice); expect_recent_payment!(bob, RecentPaymentDetails::Fulfilled, payment_id); } @@ -894,7 +898,7 @@ fn pays_for_refund_without_blinded_paths() { route_bolt12_payment(bob, &[alice], &invoice); expect_recent_payment!(bob, RecentPaymentDetails::Pending, payment_id); - claim_bolt12_payment(bob, &[alice], payment_context); + claim_bolt12_payment(bob, &[alice], payment_context, &invoice); expect_recent_payment!(bob, RecentPaymentDetails::Fulfilled, payment_id); } @@ -1132,7 +1136,7 @@ fn creates_and_pays_for_offer_with_retry() { } route_bolt12_payment(bob, &[alice], &invoice); expect_recent_payment!(bob, RecentPaymentDetails::Pending, payment_id); - claim_bolt12_payment(bob, &[alice], payment_context); + claim_bolt12_payment(bob, &[alice], payment_context, &invoice); expect_recent_payment!(bob, RecentPaymentDetails::Fulfilled, payment_id); } @@ -1203,7 +1207,7 @@ fn pays_bolt12_invoice_asynchronously() { route_bolt12_payment(bob, &[alice], &invoice); expect_recent_payment!(bob, RecentPaymentDetails::Pending, payment_id); - claim_bolt12_payment(bob, &[alice], payment_context); + claim_bolt12_payment(bob, &[alice], payment_context, &invoice); expect_recent_payment!(bob, RecentPaymentDetails::Fulfilled, payment_id); assert_eq!( @@ -1283,7 +1287,7 @@ fn creates_offer_with_blinded_path_using_unannounced_introduction_node() { route_bolt12_payment(bob, &[alice], &invoice); expect_recent_payment!(bob, RecentPaymentDetails::Pending, payment_id); - claim_bolt12_payment(bob, &[alice], payment_context); + claim_bolt12_payment(bob, &[alice], payment_context, &invoice); expect_recent_payment!(bob, RecentPaymentDetails::Fulfilled, payment_id); } @@ -2139,7 +2143,7 @@ fn fails_paying_invoice_more_than_once() { assert!(david.node.get_and_clear_pending_msg_events().is_empty()); // Complete paying the first invoice - claim_bolt12_payment(david, &[charlie, bob, alice], payment_context); + claim_bolt12_payment(david, &[charlie, bob, alice], payment_context, &invoice1); expect_recent_payment!(david, RecentPaymentDetails::Fulfilled, payment_id); } diff --git a/lightning/src/ln/outbound_payment.rs b/lightning/src/ln/outbound_payment.rs index 3033ffd3a2d..ea4fd458ce0 100644 --- a/lightning/src/ln/outbound_payment.rs +++ b/lightning/src/ln/outbound_payment.rs @@ -15,18 +15,19 @@ use bitcoin::secp256k1::{self, Secp256k1, SecretKey}; use lightning_invoice::Bolt11Invoice; use crate::blinded_path::{IntroductionNode, NodeIdLookUp}; -use crate::events::{self, PaymentFailureReason}; -use crate::types::payment::{PaymentHash, PaymentPreimage, PaymentSecret}; +use crate::events::{self, PaidBolt12Invoice, PaymentFailureReason}; use crate::ln::channel_state::ChannelDetails; use crate::ln::channelmanager::{EventCompletionAction, HTLCSource, PaymentId}; -use crate::types::features::Bolt12InvoiceFeatures; use crate::ln::onion_utils; use crate::ln::onion_utils::{DecodedOnionFailure, HTLCFailReason}; use crate::offers::invoice::Bolt12Invoice; use crate::offers::invoice_request::InvoiceRequest; +use crate::offers::static_invoice::StaticInvoice; use crate::offers::nonce::Nonce; -use crate::routing::router::{BlindedTail, InFlightHtlcs, RouteParametersConfig, Path, PaymentParameters, Route, RouteParameters, Router}; +use crate::routing::router::{BlindedTail, InFlightHtlcs, Path, PaymentParameters, Route, RouteParameters, RouteParametersConfig, Router}; use crate::sign::{EntropySource, NodeSigner, Recipient}; +use crate::types::features::Bolt12InvoiceFeatures; +use crate::types::payment::{PaymentHash, PaymentPreimage, PaymentSecret}; use crate::util::errors::APIError; use crate::util::logger::Logger; #[cfg(feature = "std")] @@ -34,10 +35,7 @@ use crate::util::time::Instant; use crate::util::ser::ReadableArgs; #[cfg(async_payments)] -use { - crate::offers::invoice::{DerivedSigningPubkey, InvoiceBuilder}, - crate::offers::static_invoice::StaticInvoice, -}; +use crate::offers::invoice::{DerivedSigningPubkey, InvoiceBuilder}; use core::fmt::{self, Display, Formatter}; use core::ops::Deref; @@ -95,6 +93,7 @@ pub(crate) enum PendingOutboundPayment { retry_strategy: Retry, route_params: RouteParameters, invoice_request: InvoiceRequest, + static_invoice: StaticInvoice, }, Retryable { retry_strategy: Option, @@ -106,6 +105,9 @@ pub(crate) enum PendingOutboundPayment { payment_metadata: Option>, keysend_preimage: Option, invoice_request: Option, + // Storing the BOLT 12 invoice here to allow Proof of Payment after + // the payment is made. + bolt12_invoice: Option, custom_tlvs: Vec<(u64, Vec)>, pending_amt_msat: u64, /// Used to track the fee paid. Present iff the payment was serialized on 0.0.103+. @@ -155,6 +157,12 @@ impl_writeable_tlv_based!(RetryableInvoiceRequest, { }); impl PendingOutboundPayment { + fn bolt12_invoice(&self) -> Option<&PaidBolt12Invoice> { + match self { + PendingOutboundPayment::Retryable { bolt12_invoice, .. } => bolt12_invoice.as_ref(), + _ => None, + } + } fn increment_attempts(&mut self) { if let PendingOutboundPayment::Retryable { attempts, .. } = self { attempts.count += 1; @@ -831,7 +839,7 @@ impl OutboundPayments { IH: Fn() -> InFlightHtlcs, SP: Fn(SendAlongPathArgs) -> Result<(), APIError>, { - self.send_payment_internal(payment_id, payment_hash, recipient_onion, None, retry_strategy, + self.send_payment_for_non_bolt12_invoice(payment_id, payment_hash, recipient_onion, None, retry_strategy, route_params, router, first_hops, &compute_inflight_htlcs, entropy_source, node_signer, best_block_height, logger, pending_events, &send_payment_along_path) } @@ -854,7 +862,7 @@ impl OutboundPayments { let preimage = payment_preimage .unwrap_or_else(|| PaymentPreimage(entropy_source.get_secure_random_bytes())); let payment_hash = PaymentHash(Sha256::hash(&preimage.0).to_byte_array()); - self.send_payment_internal(payment_id, payment_hash, recipient_onion, Some(preimage), + self.send_payment_for_non_bolt12_invoice(payment_id, payment_hash, recipient_onion, Some(preimage), retry_strategy, route_params, router, first_hops, inflight_htlcs, entropy_source, node_signer, best_block_height, logger, pending_events, send_payment_along_path) .map(|()| payment_hash) @@ -897,7 +905,7 @@ impl OutboundPayments { route_params.max_total_routing_fee_msat = Some(max_fee_msat); } - self.send_payment_internal(payment_id, payment_hash, recipient_onion, None, retry_strategy, route_params, + self.send_payment_for_non_bolt12_invoice(payment_id, payment_hash, recipient_onion, None, retry_strategy, route_params, router, first_hops, compute_inflight_htlcs, entropy_source, node_signer, best_block_height, logger, pending_events, send_payment_along_path @@ -958,8 +966,9 @@ impl OutboundPayments { if let Some(max_fee_msat) = params_config.max_total_routing_fee_msat { route_params.max_total_routing_fee_msat = Some(max_fee_msat); } + let invoice = PaidBolt12Invoice::Bolt12Invoice(invoice.clone()); self.send_payment_for_bolt12_invoice_internal( - payment_id, payment_hash, None, None, route_params, retry_strategy, router, first_hops, + payment_id, payment_hash, None, None, invoice, route_params, retry_strategy, router, first_hops, inflight_htlcs, entropy_source, node_signer, node_id_lookup, secp_ctx, best_block_height, logger, pending_events, send_payment_along_path ) @@ -970,6 +979,7 @@ impl OutboundPayments { >( &self, payment_id: PaymentId, payment_hash: PaymentHash, keysend_preimage: Option, invoice_request: Option<&InvoiceRequest>, + bolt12_invoice: PaidBolt12Invoice, mut route_params: RouteParameters, retry_strategy: Retry, router: &R, first_hops: Vec, inflight_htlcs: IH, entropy_source: &ES, node_signer: &NS, node_id_lookup: &NL, secp_ctx: &Secp256k1, best_block_height: u32, logger: &L, @@ -1032,8 +1042,8 @@ impl OutboundPayments { hash_map::Entry::Occupied(entry) => match entry.get() { PendingOutboundPayment::InvoiceReceived { .. } => { let (retryable_payment, onion_session_privs) = Self::create_pending_payment( - payment_hash, recipient_onion.clone(), keysend_preimage, None, &route, - Some(retry_strategy), payment_params, entropy_source, best_block_height + payment_hash, recipient_onion.clone(), keysend_preimage, None, Some(bolt12_invoice), &route, + Some(retry_strategy), payment_params, entropy_source, best_block_height, ); *entry.into_mut() = retryable_payment; onion_session_privs @@ -1043,7 +1053,7 @@ impl OutboundPayments { invoice_request } else { unreachable!() }; let (retryable_payment, onion_session_privs) = Self::create_pending_payment( - payment_hash, recipient_onion.clone(), keysend_preimage, Some(invreq), &route, + payment_hash, recipient_onion.clone(), keysend_preimage, Some(invreq), Some(bolt12_invoice), &route, Some(retry_strategy), payment_params, entropy_source, best_block_height ); outbounds.insert(payment_id, retryable_payment); @@ -1151,6 +1161,7 @@ impl OutboundPayments { .take() .ok_or(Bolt12PaymentError::UnexpectedInvoice)? .invoice_request, + static_invoice: invoice.clone(), }; return Ok(()) }, @@ -1179,22 +1190,22 @@ impl OutboundPayments { IH: Fn() -> InFlightHtlcs, SP: Fn(SendAlongPathArgs) -> Result<(), APIError>, { - let (payment_hash, keysend_preimage, route_params, retry_strategy, invoice_request) = + let (payment_hash, keysend_preimage, route_params, retry_strategy, invoice_request, invoice) = match self.pending_outbound_payments.lock().unwrap().entry(payment_id) { hash_map::Entry::Occupied(entry) => match entry.get() { PendingOutboundPayment::StaticInvoiceReceived { - payment_hash, route_params, retry_strategy, keysend_preimage, invoice_request, .. + payment_hash, route_params, retry_strategy, keysend_preimage, invoice_request, static_invoice, .. } => { (*payment_hash, *keysend_preimage, route_params.clone(), *retry_strategy, - invoice_request.clone()) + invoice_request.clone(), static_invoice.clone()) }, _ => return Err(Bolt12PaymentError::DuplicateInvoice), }, hash_map::Entry::Vacant(_) => return Err(Bolt12PaymentError::UnexpectedInvoice), }; - + let invoice = PaidBolt12Invoice::StaticInvoice(invoice); self.send_payment_for_bolt12_invoice_internal( - payment_id, payment_hash, Some(keysend_preimage), Some(&invoice_request), route_params, + payment_id, payment_hash, Some(keysend_preimage), Some(&invoice_request), invoice, route_params, retry_strategy, router, first_hops, inflight_htlcs, entropy_source, node_signer, node_id_lookup, secp_ctx, best_block_height, logger, pending_events, send_payment_along_path ) @@ -1318,7 +1329,7 @@ impl OutboundPayments { /// /// [`Event::PaymentPathFailed`]: crate::events::Event::PaymentPathFailed /// [`Event::PaymentFailed`]: crate::events::Event::PaymentFailed - fn send_payment_internal( + fn send_payment_for_non_bolt12_invoice( &self, payment_id: PaymentId, payment_hash: PaymentHash, recipient_onion: RecipientOnionFields, keysend_preimage: Option, retry_strategy: Retry, mut route_params: RouteParameters, router: &R, first_hops: Vec, inflight_htlcs: IH, entropy_source: &ES, @@ -1340,7 +1351,7 @@ impl OutboundPayments { let onion_session_privs = self.add_new_pending_payment(payment_hash, recipient_onion.clone(), payment_id, keysend_preimage, &route, Some(retry_strategy), - Some(route_params.payment_params.clone()), entropy_source, best_block_height) + Some(route_params.payment_params.clone()), entropy_source, best_block_height, None) .map_err(|_| { log_error!(logger, "Payment with id {} is already pending. New payment had payment hash {}", payment_id, payment_hash); @@ -1654,7 +1665,7 @@ impl OutboundPayments { let route = Route { paths: vec![path], route_params: None }; let onion_session_privs = self.add_new_pending_payment(payment_hash, RecipientOnionFields::secret_only(payment_secret), payment_id, None, &route, None, None, - entropy_source, best_block_height + entropy_source, best_block_height, None ).map_err(|e| { debug_assert!(matches!(e, PaymentSendFailure::DuplicatePayment)); ProbeSendFailure::DuplicateProbe @@ -1709,20 +1720,21 @@ impl OutboundPayments { &self, payment_hash: PaymentHash, recipient_onion: RecipientOnionFields, payment_id: PaymentId, route: &Route, retry_strategy: Option, entropy_source: &ES, best_block_height: u32 ) -> Result, PaymentSendFailure> where ES::Target: EntropySource { - self.add_new_pending_payment(payment_hash, recipient_onion, payment_id, None, route, retry_strategy, None, entropy_source, best_block_height) + self.add_new_pending_payment(payment_hash, recipient_onion, payment_id, None, route, retry_strategy, None, entropy_source, best_block_height, None) } pub(super) fn add_new_pending_payment( &self, payment_hash: PaymentHash, recipient_onion: RecipientOnionFields, payment_id: PaymentId, keysend_preimage: Option, route: &Route, retry_strategy: Option, - payment_params: Option, entropy_source: &ES, best_block_height: u32 + payment_params: Option, entropy_source: &ES, best_block_height: u32, + bolt12_invoice: Option ) -> Result, PaymentSendFailure> where ES::Target: EntropySource { let mut pending_outbounds = self.pending_outbound_payments.lock().unwrap(); match pending_outbounds.entry(payment_id) { hash_map::Entry::Occupied(_) => Err(PaymentSendFailure::DuplicatePayment), hash_map::Entry::Vacant(entry) => { let (payment, onion_session_privs) = Self::create_pending_payment( - payment_hash, recipient_onion, keysend_preimage, None, route, retry_strategy, + payment_hash, recipient_onion, keysend_preimage, None, bolt12_invoice, route, retry_strategy, payment_params, entropy_source, best_block_height ); entry.insert(payment); @@ -1734,8 +1746,8 @@ impl OutboundPayments { fn create_pending_payment( payment_hash: PaymentHash, recipient_onion: RecipientOnionFields, keysend_preimage: Option, invoice_request: Option, - route: &Route, retry_strategy: Option, payment_params: Option, - entropy_source: &ES, best_block_height: u32 + bolt12_invoice: Option, route: &Route, retry_strategy: Option, + payment_params: Option, entropy_source: &ES, best_block_height: u32 ) -> (PendingOutboundPayment, Vec<[u8; 32]>) where ES::Target: EntropySource, @@ -1757,6 +1769,7 @@ impl OutboundPayments { payment_metadata: recipient_onion.payment_metadata, keysend_preimage, invoice_request, + bolt12_invoice, custom_tlvs: recipient_onion.custom_tlvs, starting_block_height: best_block_height, total_msat: route.get_total_amount(), @@ -2016,6 +2029,7 @@ impl OutboundPayments { payment_hash, amount_msat, fee_paid_msat, + bolt12_invoice: payment.get().bolt12_invoice().cloned(), }, Some(ev_completion_action.clone()))); payment.get_mut().mark_fulfilled(); } @@ -2374,6 +2388,7 @@ impl OutboundPayments { payment_metadata: None, // only used for retries, and we'll never retry on startup keysend_preimage: None, // only used for retries, and we'll never retry on startup invoice_request: None, // only used for retries, and we'll never retry on startup + bolt12_invoice: None, // only used for retries, and we'll never retry on startup! custom_tlvs: Vec::new(), // only used for retries, and we'll never retry on startup pending_amt_msat: path_amt, pending_fee_msat: Some(path_fee), @@ -2463,6 +2478,7 @@ impl_writeable_tlv_based_enum_upgradable!(PendingOutboundPayment, (10, starting_block_height, required), (11, remaining_max_total_routing_fee_msat, option), (13, invoice_request, option), + (15, bolt12_invoice, option), (not_written, retry_strategy, (static_value, None)), (not_written, attempts, (static_value, PaymentAttempts::new())), }, @@ -2513,6 +2529,7 @@ impl_writeable_tlv_based_enum_upgradable!(PendingOutboundPayment, (4, retry_strategy, required), (6, route_params, required), (8, invoice_request, required), + (10, static_invoice, required), }, // Added in 0.1. Prior versions will drop these outbounds on downgrade, which is safe because // no HTLCs are in-flight. @@ -2613,7 +2630,7 @@ mod tests { outbound_payments.add_new_pending_payment(PaymentHash([0; 32]), RecipientOnionFields::spontaneous_empty(), PaymentId([0; 32]), None, &Route { paths: vec![], route_params: None }, Some(Retry::Attempts(1)), Some(expired_route_params.payment_params.clone()), - &&keys_manager, 0).unwrap(); + &&keys_manager, 0, None).unwrap(); outbound_payments.find_route_and_send_payment( PaymentHash([0; 32]), PaymentId([0; 32]), expired_route_params, &&router, vec![], &|| InFlightHtlcs::new(), &&keys_manager, &&keys_manager, 0, &&logger, &pending_events, @@ -2656,7 +2673,7 @@ mod tests { outbound_payments.add_new_pending_payment(PaymentHash([0; 32]), RecipientOnionFields::spontaneous_empty(), PaymentId([0; 32]), None, &Route { paths: vec![], route_params: None }, Some(Retry::Attempts(1)), Some(route_params.payment_params.clone()), - &&keys_manager, 0).unwrap(); + &&keys_manager, 0, None).unwrap(); outbound_payments.find_route_and_send_payment( PaymentHash([0; 32]), PaymentId([0; 32]), route_params, &&router, vec![], &|| InFlightHtlcs::new(), &&keys_manager, &&keys_manager, 0, &&logger, &pending_events, @@ -3143,6 +3160,7 @@ mod tests { retry_strategy: Retry::Attempts(0), route_params, invoice_request: dummy_invoice_request(), + static_invoice: dummy_static_invoice(), }; outbounds.insert(payment_id, outbound); core::mem::drop(outbounds); @@ -3190,6 +3208,7 @@ mod tests { retry_strategy: Retry::Attempts(0), route_params, invoice_request: dummy_invoice_request(), + static_invoice: dummy_static_invoice(), }; outbounds.insert(payment_id, outbound); core::mem::drop(outbounds); diff --git a/lightning/src/offers/invoice_macros.rs b/lightning/src/offers/invoice_macros.rs index 2b276a37d29..af3c2a6155e 100644 --- a/lightning/src/offers/invoice_macros.rs +++ b/lightning/src/offers/invoice_macros.rs @@ -87,7 +87,7 @@ macro_rules! invoice_builder_methods_test_common { ( $self: ident, $self_type: ty, $invoice_fields: expr, $return_type: ty, $return_value: expr $(, $self_mut: tt)? ) => { - #[cfg_attr(c_bindings, allow(dead_code))] + #[allow(dead_code)] // TODO: mode to `#[cfg_attr(c_bindings, allow(dead_code))]` once we remove the `async_payments` cfg flag pub(crate) fn features_unchecked( $($self_mut)* $self: $self_type, features: Bolt12InvoiceFeatures ) -> $return_type { diff --git a/lightning/src/offers/static_invoice.rs b/lightning/src/offers/static_invoice.rs index e221a11913a..d48a83eb9b7 100644 --- a/lightning/src/offers/static_invoice.rs +++ b/lightning/src/offers/static_invoice.rs @@ -33,7 +33,9 @@ use crate::offers::offer::{ }; use crate::offers::parse::{Bolt12ParseError, Bolt12SemanticError, ParsedMessage}; use crate::types::features::{Bolt12InvoiceFeatures, OfferFeatures}; -use crate::util::ser::{CursorReadable, Iterable, WithoutLength, Writeable, Writer}; +use crate::util::ser::{ + CursorReadable, Iterable, LengthLimitedRead, LengthReadable, WithoutLength, Writeable, Writer, +}; use crate::util::string::PrintableString; use bitcoin::address::Address; use bitcoin::constants::ChainHash; @@ -70,6 +72,14 @@ pub struct StaticInvoice { signature: Signature, } +impl PartialEq for StaticInvoice { + fn eq(&self, other: &Self) -> bool { + self.bytes.eq(&other.bytes) + } +} + +impl Eq for StaticInvoice {} + /// The contents of a [`StaticInvoice`] for responding to an [`Offer`]. /// /// [`Offer`]: crate::offers::offer::Offer @@ -534,6 +544,12 @@ impl Writeable for StaticInvoice { WithoutLength(&self.bytes).write(writer) } } +impl LengthReadable for StaticInvoice { + fn read_from_fixed_length_buffer(r: &mut R) -> Result { + let bytes: WithoutLength> = LengthReadable::read_from_fixed_length_buffer(r)?; + Self::try_from(bytes.0).map_err(|_| DecodeError::InvalidValue) + } +} impl TryFrom> for StaticInvoice { type Error = Bolt12ParseError; diff --git a/lightning/src/offers/test_utils.rs b/lightning/src/offers/test_utils.rs index 4b088705a46..9888f0303af 100644 --- a/lightning/src/offers/test_utils.rs +++ b/lightning/src/offers/test_utils.rs @@ -12,8 +12,10 @@ use bitcoin::secp256k1::schnorr::Signature; use bitcoin::secp256k1::{Keypair, PublicKey, Secp256k1, SecretKey}; +use crate::blinded_path::message::BlindedMessagePath; use crate::blinded_path::payment::{BlindedPayInfo, BlindedPaymentPath}; use crate::blinded_path::BlindedHop; +use crate::ln::inbound_payment::ExpandedKey; use crate::offers::merkle::TaggedHash; use crate::sign::EntropySource; use crate::types::features::BlindedHopFeatures; @@ -23,6 +25,10 @@ use core::time::Duration; #[allow(unused_imports)] use crate::prelude::*; +use super::nonce::Nonce; +use super::offer::OfferBuilder; +use super::static_invoice::{StaticInvoice, StaticInvoiceBuilder}; + pub(crate) fn fail_sign>(_message: &T) -> Result { Err(()) } @@ -120,3 +126,42 @@ impl EntropySource for FixedEntropy { [42; 32] } } + +pub fn blinded_path() -> BlindedMessagePath { + BlindedMessagePath::from_blinded_path( + pubkey(40), + pubkey(41), + vec![ + BlindedHop { blinded_node_id: pubkey(42), encrypted_payload: vec![0; 43] }, + BlindedHop { blinded_node_id: pubkey(43), encrypted_payload: vec![0; 44] }, + ], + ) +} + +pub fn dummy_static_invoice() -> StaticInvoice { + let node_id = recipient_pubkey(); + let payment_paths = payment_paths(); + let now = now(); + let expanded_key = ExpandedKey::new([42; 32]); + let entropy = FixedEntropy {}; + let nonce = Nonce::from_entropy_source(&entropy); + let secp_ctx = Secp256k1::new(); + + let offer = OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, nonce, &secp_ctx) + .path(blinded_path()) + .build() + .unwrap(); + + StaticInvoiceBuilder::for_offer_using_derived_keys( + &offer, + payment_paths.clone(), + vec![blinded_path()], + now, + &expanded_key, + nonce, + &secp_ctx, + ) + .unwrap() + .build_and_sign(&secp_ctx) + .unwrap() +}