diff --git a/fuzz/src/invoice_request_deser.rs b/fuzz/src/invoice_request_deser.rs index a21303debd7..c4b31942843 100644 --- a/fuzz/src/invoice_request_deser.rs +++ b/fuzz/src/invoice_request_deser.rs @@ -104,6 +104,7 @@ fn build_response( let payment_context = PaymentContext::Bolt12Offer(Bolt12OfferContext { offer_id: OfferId([42; 32]), invoice_request: invoice_request_fields, + payment_metadata: None, }); let payee_tlvs = ReceiveTlvs { payment_secret: PaymentSecret([42; 32]), diff --git a/fuzz/src/refund_deser.rs b/fuzz/src/refund_deser.rs index 446ac704455..57addeed698 100644 --- a/fuzz/src/refund_deser.rs +++ b/fuzz/src/refund_deser.rs @@ -69,7 +69,7 @@ fn build_response( ) -> Result { let entropy_source = Randomness {}; let receive_auth_key = ReceiveAuthKey([41; 32]); - let payment_context = PaymentContext::Bolt12Refund(Bolt12RefundContext {}); + let payment_context = PaymentContext::Bolt12Refund(Bolt12RefundContext { payment_metadata: None }); let payee_tlvs = ReceiveTlvs { payment_secret: PaymentSecret([42; 32]), payment_constraints: PaymentConstraints { diff --git a/lightning/src/blinded_path/message.rs b/lightning/src/blinded_path/message.rs index 7bcbe80a965..7e182de3233 100644 --- a/lightning/src/blinded_path/message.rs +++ b/lightning/src/blinded_path/message.rs @@ -9,6 +9,8 @@ //! Data structures and methods for constructing [`BlindedMessagePath`]s to send a message over. +use alloc::collections::BTreeMap; + use bitcoin::secp256k1::{self, PublicKey, Secp256k1, SecretKey}; #[allow(unused_imports)] @@ -29,7 +31,9 @@ use crate::routing::gossip::{NodeId, ReadOnlyNetworkGraph}; use crate::sign::{EntropySource, NodeSigner, ReceiveAuthKey, Recipient}; use crate::types::payment::PaymentHash; use crate::util::scid_utils; -use crate::util::ser::{FixedLengthReader, LengthReadableArgs, Readable, Writeable, Writer}; +use crate::util::ser::{ + BigSizeKeyedMap, FixedLengthReader, LengthReadableArgs, Readable, Writeable, Writer, +}; use core::time::Duration; use core::{cmp, mem}; @@ -391,6 +395,23 @@ pub enum OffersContext { /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest /// [`Offer`]: crate::offers::offer::Offer nonce: Nonce, + + /// Additional data about this payment which is not used in LDK and can be used for any + /// purpose. + /// + /// This is analogous to the BOLT 11 [`RecipientOnionFields::payment_metadata`] (which is + /// provided to payers via [`Bolt11Invoice::payment_metadata`]) and can be used any time data + /// needs to be "stored" by a payment recipient for their own internal use, provided back to + /// them with the payment. + /// + /// Note that because this is included in the payment onion, its size must be tightly + /// constrained. More than a few hundred bytes and the payment will be entirely unpayable (with + /// limited routing options as size increases). Further, any data placed here will increase + /// the size of the offer which may make it difficult to fit in QR codes. + /// + /// [`RecipientOnionFields::payment_metadata`]: crate::ln::outbound_payment::RecipientOnionFields::payment_metadata + /// [`Bolt11Invoice::payment_metadata`]: lightning_invoice::Bolt11Invoice::payment_metadata + payment_metadata: Option>>, }, /// Context used by a [`BlindedMessagePath`] within the [`Offer`] of an async recipient. /// @@ -648,6 +669,7 @@ impl_writeable_tlv_based_enum!(MessageContext, impl_writeable_tlv_based_enum!(OffersContext, (0, InvoiceRequest) => { (0, nonce, required), + (1, payment_metadata, (option, encoding: (BTreeMap>, BigSizeKeyedMap))), }, (1, OutboundPaymentForRefund) => { (0, payment_id, required), diff --git a/lightning/src/blinded_path/payment.rs b/lightning/src/blinded_path/payment.rs index f06c91bf6e0..c492bc905fa 100644 --- a/lightning/src/blinded_path/payment.rs +++ b/lightning/src/blinded_path/payment.rs @@ -9,6 +9,8 @@ //! Data structures and methods for constructing [`BlindedPaymentPath`]s to send a payment over. +use alloc::collections::BTreeMap; + use bitcoin::secp256k1::ecdh::SharedSecret; use bitcoin::secp256k1::{self, PublicKey, Secp256k1, SecretKey}; @@ -29,8 +31,8 @@ use crate::types::features::BlindedHopFeatures; use crate::types::payment::PaymentSecret; use crate::types::routing::RoutingFees; use crate::util::ser::{ - FixedLengthReader, HighZeroBytesDroppedBigSize, LengthReadableArgs, Readable, WithoutLength, - Writeable, Writer, + BigSizeKeyedMap, FixedLengthReader, HighZeroBytesDroppedBigSize, LengthReadableArgs, Readable, + WithoutLength, Writeable, Writer, }; #[allow(unused_imports)] @@ -572,6 +574,16 @@ pub enum PaymentContext { /// [`Refund`]: crate::offers::refund::Refund Bolt12Refund(Bolt12RefundContext), } +impl PaymentContext { + /// Returns the additional payment metadata stored alongside this payment context, if any. + pub fn payment_metadata(&self) -> Option<&BTreeMap>> { + match self { + Self::Bolt12Offer(Bolt12OfferContext { payment_metadata, .. }) + | Self::AsyncBolt12Offer(AsyncBolt12OfferContext { payment_metadata, .. }) + | Self::Bolt12Refund(Bolt12RefundContext { payment_metadata, .. }) => payment_metadata.as_ref(), + } + } +} // Used when writing PaymentContext in Event::PaymentClaimable to avoid cloning. pub(crate) enum PaymentContextRef<'a> { @@ -594,6 +606,22 @@ pub struct Bolt12OfferContext { /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest /// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice pub invoice_request: InvoiceRequestFields, + + /// Additional data about this payment which is not used in LDK and can be used for any + /// purpose. + /// + /// This is analogous to the BOLT 11 [`RecipientOnionFields::payment_metadata`] (which is + /// provided to payers via [`Bolt11Invoice::payment_metadata`]) and can be used any time data + /// needs to be "stored" by a payment recipient for their own internal use, provided back to + /// them with the payment. + /// + /// Note that because this is included in the payment onion, its size must be tightly + /// constrained. More than a few hundred bytes and the payment will be entirely unpayable (with + /// limited routing options as size increases). + /// + /// [`RecipientOnionFields::payment_metadata`]: crate::ln::outbound_payment::RecipientOnionFields::payment_metadata + /// [`Bolt11Invoice::payment_metadata`]: lightning_invoice::Bolt11Invoice::payment_metadata + pub payment_metadata: Option>>, } /// The context of a payment made for a static invoice requested from a BOLT 12 [`Offer`]. @@ -606,13 +634,45 @@ pub struct AsyncBolt12OfferContext { /// /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest pub offer_nonce: Nonce, + + /// Additional data about this payment which is not used in LDK and can be used for any + /// purpose. + /// + /// This is analogous to the BOLT 11 [`RecipientOnionFields::payment_metadata`] (which is + /// provided to payers via [`Bolt11Invoice::payment_metadata`]) and can be used any time data + /// needs to be "stored" by a payment recipient for their own internal use, provided back to + /// them with the payment. + /// + /// Note that because this is included in the payment onion, its size must be tightly + /// constrained. More than a few hundred bytes and the payment will be entirely unpayable (with + /// limited routing options as size increases). + /// + /// [`RecipientOnionFields::payment_metadata`]: crate::ln::outbound_payment::RecipientOnionFields::payment_metadata + /// [`Bolt11Invoice::payment_metadata`]: lightning_invoice::Bolt11Invoice::payment_metadata + pub payment_metadata: Option>>, } /// The context of a payment made for an invoice sent for a BOLT 12 [`Refund`]. /// /// [`Refund`]: crate::offers::refund::Refund #[derive(Clone, Debug, Eq, PartialEq)] -pub struct Bolt12RefundContext {} +pub struct Bolt12RefundContext { + /// Additional data about this payment which is not used in LDK and can be used for any + /// purpose. + /// + /// This is analogous to the BOLT 11 [`RecipientOnionFields::payment_metadata`] (which is + /// provided to payers via [`Bolt11Invoice::payment_metadata`]) and can be used any time data + /// needs to be "stored" by a payment recipient for their own internal use, provided back to + /// them with the payment. + /// + /// Note that because this is included in the payment onion, its size must be tightly + /// constrained. More than a few hundred bytes and the payment will be entirely unpayable (with + /// limited routing options as size increases). + /// + /// [`RecipientOnionFields::payment_metadata`]: crate::ln::outbound_payment::RecipientOnionFields::payment_metadata + /// [`Bolt11Invoice::payment_metadata`]: lightning_invoice::Bolt11Invoice::payment_metadata + pub payment_metadata: Option>>, +} impl TryFrom for PaymentRelay { type Error = (); @@ -1031,14 +1091,18 @@ impl<'a> Writeable for PaymentContextRef<'a> { impl_writeable_tlv_based!(Bolt12OfferContext, { (0, offer_id, required), + (1, payment_metadata, (option, encoding: (BTreeMap>, BigSizeKeyedMap))), (2, invoice_request, required), }); impl_writeable_tlv_based!(AsyncBolt12OfferContext, { (0, offer_nonce, required), + (1, payment_metadata, (option, encoding: (BTreeMap>, BigSizeKeyedMap))), }); -impl_writeable_tlv_based!(Bolt12RefundContext, {}); +impl_writeable_tlv_based!(Bolt12RefundContext, { + (1, payment_metadata, (option, encoding: (BTreeMap>, BigSizeKeyedMap))), +}); #[cfg(test)] mod tests { @@ -1097,7 +1161,9 @@ mod tests { let recv_tlvs = ReceiveTlvs { payment_secret: PaymentSecret([0; 32]), payment_constraints: PaymentConstraints { max_cltv_expiry: 0, htlc_minimum_msat: 1 }, - payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext {}), + payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext { + payment_metadata: None, + }), }; let htlc_maximum_msat = 100_000; let blinded_payinfo = @@ -1115,7 +1181,9 @@ mod tests { let recv_tlvs = ReceiveTlvs { payment_secret: PaymentSecret([0; 32]), payment_constraints: PaymentConstraints { max_cltv_expiry: 0, htlc_minimum_msat: 1 }, - payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext {}), + payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext { + payment_metadata: None, + }), }; let blinded_payinfo = super::compute_payinfo::( &[], @@ -1178,7 +1246,9 @@ mod tests { let recv_tlvs = ReceiveTlvs { payment_secret: PaymentSecret([0; 32]), payment_constraints: PaymentConstraints { max_cltv_expiry: 0, htlc_minimum_msat: 3 }, - payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext {}), + payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext { + payment_metadata: None, + }), }; let htlc_maximum_msat = 100_000; let blinded_payinfo = super::compute_payinfo( @@ -1238,7 +1308,9 @@ mod tests { let recv_tlvs = ReceiveTlvs { payment_secret: PaymentSecret([0; 32]), payment_constraints: PaymentConstraints { max_cltv_expiry: 0, htlc_minimum_msat: 1 }, - payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext {}), + payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext { + payment_metadata: None, + }), }; let htlc_minimum_msat = 3798; assert!(super::compute_payinfo( @@ -1309,7 +1381,9 @@ mod tests { let recv_tlvs = ReceiveTlvs { payment_secret: PaymentSecret([0; 32]), payment_constraints: PaymentConstraints { max_cltv_expiry: 0, htlc_minimum_msat: 1 }, - payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext {}), + payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext { + payment_metadata: None, + }), }; let blinded_payinfo = super::compute_payinfo( diff --git a/lightning/src/ln/async_payments_tests.rs b/lightning/src/ln/async_payments_tests.rs index bd07d13c13d..52c947fb713 100644 --- a/lightning/src/ln/async_payments_tests.rs +++ b/lightning/src/ln/async_payments_tests.rs @@ -7,6 +7,8 @@ // You may not use this file except in accordance with one or both of these // licenses. +use alloc::collections::BTreeMap; + use crate::blinded_path::message::{ BlindedMessagePath, MessageContext, NextMessageHop, OffersContext, }; @@ -299,6 +301,7 @@ fn create_static_invoice_builder<'a>( relative_expiry_secs, recipient.node.list_usable_channels(), recipient.node.test_get_peers_for_blinded_path(), + None, ) .unwrap() } @@ -314,7 +317,10 @@ fn create_static_invoice( .create_blinded_paths( always_online_counterparty.node.get_our_node_id(), always_online_counterparty.keys_manager.get_receive_auth_key(), - MessageContext::Offers(OffersContext::InvoiceRequest { nonce: Nonce([42; 16]) }), + MessageContext::Offers(OffersContext::InvoiceRequest { + nonce: Nonce([42; 16]), + payment_metadata: None, + }), Vec::new(), &secp_ctx, ) @@ -685,7 +691,10 @@ fn static_invoice_unknown_required_features() { .create_blinded_paths( nodes[1].node.get_our_node_id(), nodes[1].keys_manager.get_receive_auth_key(), - MessageContext::Offers(OffersContext::InvoiceRequest { nonce: Nonce([42; 16]) }), + MessageContext::Offers(OffersContext::InvoiceRequest { + nonce: Nonce([42; 16]), + payment_metadata: None, + }), Vec::new(), &secp_ctx, ) @@ -1150,6 +1159,88 @@ fn async_receive_flow_success() { assert_eq!(res, Some(PaidBolt12Invoice::StaticInvoice(static_invoice))); } +#[test] +fn async_payment_delivers_payment_metadata() { + // Test that `payment_metadata` set in the `AsyncBolt12OfferContext` of a static invoice's + // blinded payment paths is surfaced via `Event::PaymentClaimable` when the async recipient + // receives the keysend payment. + let chanmon_cfgs = create_chanmon_cfgs(3); + let node_cfgs = create_node_cfgs(3, &chanmon_cfgs); + + let mut allow_priv_chan_fwds_cfg = test_default_channel_config(); + allow_priv_chan_fwds_cfg.accept_forwards_to_priv_channels = true; + let node_chanmgrs = + create_node_chanmgrs(3, &node_cfgs, &[None, Some(allow_priv_chan_fwds_cfg), None]); + + let nodes = create_network(3, &node_cfgs, &node_chanmgrs); + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0); + create_unannounced_chan_between_nodes_with_value(&nodes, 1, 2, 1_000_000, 0); + + let recipient_id = vec![42; 32]; + let inv_server_paths = + nodes[1].node.blinded_paths_for_async_recipient(recipient_id.clone(), None).unwrap(); + nodes[2].node.set_paths_to_static_invoice_server(inv_server_paths).unwrap(); + expect_offer_paths_requests(&nodes[2], &[&nodes[0], &nodes[1]]); + + // Configure the recipient's router to inject `payment_metadata` into the + // `AsyncBolt12OfferContext` of the static invoice's blinded payment paths. The + // `pass_static_invoice_server_messages` flow below builds the static invoice via this router, + // at which point the override is consumed. + let mut expected_metadata = BTreeMap::new(); + expected_metadata.insert(0u64, vec![1, 2, 3, 4]); + expected_metadata.insert(7u64, vec![0xab, 0xcd]); + nodes[2].router.set_next_payment_context_metadata(expected_metadata.clone()); + + let invoice_flow_res = + pass_static_invoice_server_messages(&nodes[1], &nodes[2], recipient_id.clone()); + let static_invoice = invoice_flow_res.invoice; + let offer = nodes[2].node.get_async_receive_offer().unwrap(); + let amt_msat = 5000; + let payment_id = PaymentId([1; 32]); + nodes[0].node.pay_for_offer(&offer, Some(amt_msat), payment_id, Default::default()).unwrap(); + let release_held_htlc_om = pass_async_payments_oms( + static_invoice.clone(), + &nodes[0], + &nodes[1], + &nodes[2], + recipient_id, + invoice_flow_res.invoice_request_path, + ) + .1; + nodes[0] + .onion_messenger + .handle_onion_message(nodes[2].node.get_our_node_id(), &release_held_htlc_om); + + let mut events = nodes[0].node.get_and_clear_pending_msg_events(); + assert_eq!(events.len(), 1); + let ev = remove_first_msg_event_to_node(&nodes[1].node.get_our_node_id(), &mut events); + let payment_hash = extract_payment_hash(&ev); + check_added_monitors(&nodes[0], 1); + + let route: &[&[&Node]] = &[&[&nodes[1], &nodes[2]]]; + let args = PassAlongPathArgs::new(&nodes[0], route[0], amt_msat, payment_hash, ev) + .with_dummy_tlvs(&[DummyTlvs::default(); DEFAULT_PAYMENT_DUMMY_HOPS]); + let claimable_ev = do_pass_along_path(args).unwrap(); + + // Verify the `payment_metadata` we injected is surfaced via the `Bolt12OfferContext` of + // the `PaymentPurpose`. The recipient converts `AsyncBolt12OfferContext` to + // `Bolt12OfferContext` when constructing the `PaymentPurpose` for keysend payments. + match &claimable_ev { + Event::PaymentClaimable { + purpose: PaymentPurpose::Bolt12OfferPayment { payment_context, .. }, + .. + } => { + assert_eq!(payment_context.payment_metadata.as_ref(), Some(&expected_metadata)); + }, + _ => panic!("Unexpected event: {:?}", claimable_ev), + } + + let keysend_preimage = extract_payment_preimage(&claimable_ev); + let (res, _) = + claim_payment_along_route(ClaimAlongRouteArgs::new(&nodes[0], route, keysend_preimage)); + assert_eq!(res, Some(PaidBolt12Invoice::StaticInvoice(static_invoice))); +} + #[cfg_attr(feature = "std", ignore)] #[test] fn expired_static_invoice_fail() { @@ -1591,6 +1682,7 @@ fn reject_bad_payment_secret() { PaymentContext::AsyncBolt12Offer(AsyncBolt12OfferContext { // We don't reach the point of checking the invreq nonce due to the invalid payment secret offer_nonce: Nonce([i; Nonce::LENGTH]), + payment_metadata: None, }), u32::MAX, ) @@ -1669,7 +1761,7 @@ fn invalid_async_receive_with_retry( .create_blinded_paths( nodes[1].node.get_our_node_id(), nodes[1].keys_manager.get_receive_auth_key(), - MessageContext::Offers(OffersContext::InvoiceRequest { nonce: Nonce([42; 16]) }), + MessageContext::Offers(OffersContext::InvoiceRequest { nonce: Nonce([42; 16]), payment_metadata: None }), Vec::new(), &secp_ctx, ) @@ -3123,7 +3215,10 @@ fn intercepted_hold_htlc() { .unwrap(); let mut offer_nonce = Nonce([0; Nonce::LENGTH]); offer_nonce.0.copy_from_slice(&hardcoded_random_bytes[..Nonce::LENGTH]); - let payment_context = PaymentContext::AsyncBolt12Offer(AsyncBolt12OfferContext { offer_nonce }); + let payment_context = PaymentContext::AsyncBolt12Offer(AsyncBolt12OfferContext { + offer_nonce, + payment_metadata: None, + }); let blinded_payment_path_with_jit_channel_scid = recipient .node .flow diff --git a/lightning/src/ln/blinded_payment_tests.rs b/lightning/src/ln/blinded_payment_tests.rs index 621c5103353..32c0709ed5c 100644 --- a/lightning/src/ln/blinded_payment_tests.rs +++ b/lightning/src/ln/blinded_payment_tests.rs @@ -83,7 +83,7 @@ pub fn blinded_payment_path( htlc_minimum_msat: intro_node_min_htlc_opt.unwrap_or_else(|| channel_upds.last().unwrap().htlc_minimum_msat), }, - payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext {}), + payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext { payment_metadata: None }), }; let receive_auth_key = keys_manager.get_receive_auth_key(); @@ -172,7 +172,7 @@ fn do_one_hop_blinded_path(success: bool) { max_cltv_expiry: u32::max_value(), htlc_minimum_msat: chan_upd.htlc_minimum_msat, }, - payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext {}), + payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext { payment_metadata: None }), }; let receive_auth_key = chanmon_cfgs[1].keys_manager.get_receive_auth_key(); @@ -216,7 +216,9 @@ fn one_hop_blinded_path_with_dummy_hops() { max_cltv_expiry: u32::max_value(), htlc_minimum_msat: chan_upd.htlc_minimum_msat, }, - payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext {}), + payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext { + payment_metadata: None, + }), }; let receive_auth_key = chanmon_cfgs[1].keys_manager.get_receive_auth_key(); let dummy_tlvs = [DummyTlvs::default(); 2]; @@ -296,7 +298,7 @@ fn mpp_to_one_hop_blinded_path() { max_cltv_expiry: u32::max_value(), htlc_minimum_msat: chan_upd_1_3.htlc_minimum_msat, }, - payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext {}), + payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext { payment_metadata: None }), }; let receive_auth_key = chanmon_cfgs[3].keys_manager.get_receive_auth_key(); let blinded_path = BlindedPaymentPath::new( @@ -1419,7 +1421,7 @@ fn custom_tlvs_to_blinded_path() { max_cltv_expiry: u32::max_value(), htlc_minimum_msat: chan_upd.htlc_minimum_msat, }, - payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext {}), + payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext { payment_metadata: None }), }; let receive_auth_key = chanmon_cfgs[1].keys_manager.get_receive_auth_key(); @@ -1473,7 +1475,7 @@ fn fails_receive_tlvs_authentication() { max_cltv_expiry: u32::max_value(), htlc_minimum_msat: chan_upd.htlc_minimum_msat, }, - payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext {}), + payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext { payment_metadata: None }), }; let receive_auth_key = chanmon_cfgs[1].keys_manager.get_receive_auth_key(); @@ -1503,7 +1505,7 @@ fn fails_receive_tlvs_authentication() { max_cltv_expiry: u32::max_value(), htlc_minimum_msat: chan_upd.htlc_minimum_msat, }, - payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext {}), + payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext { payment_metadata: None }), }; // Use a mismatched ReceiveAuthKey to force auth failure: let mismatched_receive_auth_key = ReceiveAuthKey([0u8; 32]); @@ -2286,7 +2288,7 @@ fn do_test_trampoline_single_hop_receive(success: bool) { max_cltv_expiry: u32::max_value(), htlc_minimum_msat: amt_msat, }, - payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext {}), + payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext { payment_metadata: None }), }; let receive_auth_key = nodes[2].keys_manager.get_receive_auth_key(); let blinded_path = BlindedPaymentPath::new(&[], carol_node_id, receive_auth_key, payee_tlvs, u64::MAX, 0, nodes[2].keys_manager, &secp_ctx).unwrap(); @@ -2607,7 +2609,9 @@ fn do_test_trampoline_relay(blinded: bool, test_case: TrampolineTestCase) { max_cltv_expiry: u32::max_value(), htlc_minimum_msat: original_amt_msat, }, - payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext {}), + payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext { + payment_metadata: None, + }), }, original_trampoline_cltv, excess_final_cltv, diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 1f32423507f..ec09235d7f9 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -8665,7 +8665,7 @@ impl< }, OnionPayload::Spontaneous(keysend_preimage) => { let purpose = if let Some(PaymentContext::AsyncBolt12Offer( - AsyncBolt12OfferContext { offer_nonce }, + AsyncBolt12OfferContext { offer_nonce, payment_metadata }, )) = payment_context { let payment_data = match payment_data { @@ -8707,6 +8707,7 @@ impl< PaymentContext::Bolt12Offer(Bolt12OfferContext { offer_id: verified_invreq.offer_id(), invoice_request: verified_invreq.fields(), + payment_metadata, }); let from_parts_res = events::PaymentPurpose::from_parts( Some(keysend_preimage), @@ -14933,6 +14934,7 @@ impl< self.create_inbound_payment(Some(amount_msats), relative_expiry, None) .map_err(|()| Bolt12SemanticError::InvalidAmount) }, + None, )?; let invoice = builder.allow_mpp().build_and_sign(secp_ctx)?; @@ -17090,6 +17092,13 @@ impl< None => return None, }; + let payment_metadata = + if let Some(OffersContext::InvoiceRequest { payment_metadata, .. }) = &context { + payment_metadata.clone() + } else { + None + }; + let invoice_request = match self.flow.verify_invoice_request(invoice_request, context) { Ok(InvreqResponseInstructions::SendInvoice(invoice_request)) => invoice_request, Ok(InvreqResponseInstructions::SendStaticInvoice { recipient_id, invoice_slot, invoice_request }) => { @@ -17117,6 +17126,7 @@ impl< &request, self.list_usable_channels(), get_payment_info, + payment_metadata, ); match result { @@ -17141,6 +17151,7 @@ impl< &request, self.list_usable_channels(), get_payment_info, + payment_metadata, ); match result { diff --git a/lightning/src/ln/features.rs b/lightning/src/ln/features.rs index b568d5595a5..a4e7fc15394 100644 --- a/lightning/src/ln/features.rs +++ b/lightning/src/ln/features.rs @@ -81,6 +81,12 @@ macro_rules! impl_feature_write_without_length { } } + impl Writeable for WithoutLength<&&$features> { + fn write(&self, w: &mut W) -> Result<(), io::Error> { + write_be(w, self.0.le_flags()) + } + } + impl Readable for WithoutLength<$features> { fn read(r: &mut R) -> Result { let v = io_extras::read_to_end(r)?; diff --git a/lightning/src/ln/max_payment_path_len_tests.rs b/lightning/src/ln/max_payment_path_len_tests.rs index 0515a5290d7..4d0abb6bfac 100644 --- a/lightning/src/ln/max_payment_path_len_tests.rs +++ b/lightning/src/ln/max_payment_path_len_tests.rs @@ -222,7 +222,9 @@ fn one_hop_blinded_path_with_custom_tlv() { max_cltv_expiry: u32::max_value(), htlc_minimum_msat: chan_upd_1_2.htlc_minimum_msat, }, - payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext {}), + payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext { + payment_metadata: None, + }), }; let receive_auth_key = chanmon_cfgs[2].keys_manager.get_receive_auth_key(); let mut secp_ctx = Secp256k1::new(); diff --git a/lightning/src/ln/msgs.rs b/lightning/src/ln/msgs.rs index 6210d26893a..5643bfd9498 100644 --- a/lightning/src/ln/msgs.rs +++ b/lightning/src/ln/msgs.rs @@ -763,10 +763,10 @@ pub struct UpdateAddHTLC { struct AccountableBool(T); -impl Writeable for AccountableBool { +impl Writeable for AccountableBool<&bool> { #[inline] fn write(&self, writer: &mut W) -> Result<(), io::Error> { - let wire_value = if self.0 { 7u8 } else { 0u8 }; + let wire_value = if *self.0 { 7u8 } else { 0u8 }; writer.write_all(&[wire_value]) } } diff --git a/lightning/src/ln/offers_tests.rs b/lightning/src/ln/offers_tests.rs index de08af5d276..5eaf64b838b 100644 --- a/lightning/src/ln/offers_tests.rs +++ b/lightning/src/ln/offers_tests.rs @@ -42,13 +42,15 @@ //! Nodes without channels are disconnected and connected as needed to ensure that deterministic //! blinded paths are used. +use alloc::collections::BTreeMap; + use bitcoin::network::Network; use bitcoin::secp256k1::{PublicKey, Secp256k1}; use core::time::Duration; use crate::blinded_path::IntroductionNode; use crate::blinded_path::message::BlindedMessagePath; use crate::blinded_path::payment::{Bolt12OfferContext, Bolt12RefundContext, DummyTlvs, PaymentContext}; -use crate::blinded_path::message::OffersContext; +use crate::blinded_path::message::{MessageContext, OffersContext}; use crate::events::{ClosureReason, Event, HTLCHandlingFailureType, PaidBolt12Invoice, PaymentFailureReason, PaymentPurpose}; use crate::ln::channelmanager::{PaymentId, RecentPaymentDetails, self}; use crate::ln::outbound_payment::{Bolt12PaymentError, RecipientOnionFields, Retry}; @@ -60,8 +62,9 @@ use crate::offers::invoice::Bolt12Invoice; use crate::offers::invoice_error::InvoiceError; use crate::offers::invoice_request::{InvoiceRequest, InvoiceRequestFields, InvoiceRequestVerifiedFromOffer}; use crate::offers::nonce::Nonce; +use crate::offers::offer::OfferBuilder; use crate::offers::parse::Bolt12SemanticError; -use crate::onion_message::messenger::{DefaultMessageRouter, Destination, MessageSendInstructions, NodeIdMessageRouter, NullMessageRouter, PeeledOnion, DUMMY_HOPS_PATH_LENGTH, QR_CODED_DUMMY_HOPS_PATH_LENGTH}; +use crate::onion_message::messenger::{DefaultMessageRouter, Destination, MessageRouter, MessageSendInstructions, NodeIdMessageRouter, NullMessageRouter, PeeledOnion, DUMMY_HOPS_PATH_LENGTH, QR_CODED_DUMMY_HOPS_PATH_LENGTH}; use crate::onion_message::offers::OffersMessage; use crate::routing::gossip::{NodeAlias, NodeId}; use crate::routing::router::{DEFAULT_PAYMENT_DUMMY_HOPS, PaymentParameters, RouteParameters, RouteParametersConfig}; @@ -256,7 +259,7 @@ fn claim_bolt12_payment_with_extra_fees<'a, 'b, 'c>( fn extract_offer_nonce<'a, 'b, 'c>(node: &Node<'a, 'b, 'c>, message: &OnionMessage) -> Nonce { match node.onion_messenger.peel_onion_message(message) { - Ok(PeeledOnion::Offers(_, Some(OffersContext::InvoiceRequest { nonce }), _)) => nonce, + Ok(PeeledOnion::Offers(_, Some(OffersContext::InvoiceRequest { nonce, payment_metadata: _ }), _)) => nonce, Ok(PeeledOnion::Offers(_, context, _)) => panic!("Unexpected onion message context: {:?}", context), Ok(PeeledOnion::Forward(_, _)) => panic!("Unexpected onion message forward"), Ok(_) => panic!("Unexpected onion message"), @@ -728,6 +731,7 @@ fn creates_and_pays_for_offer_using_two_hop_blinded_path() { payer_note_truncated: None, human_readable_name: None, }, + payment_metadata: None, }); assert_eq!(invoice_request.amount_msats(), Some(10_000_000)); assert_ne!(invoice_request.payer_signing_pubkey(), david_id); @@ -814,7 +818,7 @@ fn creates_and_pays_for_refund_using_two_hop_blinded_path() { } expect_recent_payment!(david, RecentPaymentDetails::AwaitingInvoice, payment_id); - let payment_context = PaymentContext::Bolt12Refund(Bolt12RefundContext {}); + let payment_context = PaymentContext::Bolt12Refund(Bolt12RefundContext { payment_metadata: None }); let expected_invoice = alice.node.request_refund_payment(&refund).unwrap(); connect_peers(alice, charlie); @@ -886,6 +890,7 @@ fn creates_and_pays_for_offer_using_one_hop_blinded_path() { payer_note_truncated: None, human_readable_name: None, }, + payment_metadata: None, }); assert_eq!(invoice_request.amount_msats(), Some(10_000_000)); assert_ne!(invoice_request.payer_signing_pubkey(), bob_id); @@ -910,6 +915,158 @@ fn creates_and_pays_for_offer_using_one_hop_blinded_path() { expect_recent_payment!(bob, RecentPaymentDetails::Fulfilled, payment_id); } +/// Checks that a `Router` can attach `payment_metadata` to the [`PaymentContext`] of a blinded +/// payment path while building it in response to an invoice request, and that the metadata is +/// surfaced back via [`Event::PaymentClaimable`] when the payment is received. +#[test] +fn router_modifies_payment_metadata_in_blinded_path() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 10_000_000, 1_000_000_000); + + let alice = &nodes[0]; + let alice_id = alice.node.get_our_node_id(); + let bob = &nodes[1]; + let bob_id = bob.node.get_our_node_id(); + + // Configure Alice's router to inject `payment_metadata` into the `PaymentContext` of the + // `ReceiveTlvs` it builds blinded payment paths from. This simulates a recipient-side router + // that ties extra recipient data (e.g. an order ID) to the blinded path created in response to + // an inbound invoice request. + let mut expected_metadata = BTreeMap::new(); + expected_metadata.insert(0u64, vec![1, 2, 3, 4]); + expected_metadata.insert(7u64, vec![0xab, 0xcd]); + alice.router.set_next_payment_context_metadata(expected_metadata.clone()); + + let offer = alice.node + .create_offer_builder().unwrap() + .amount_msats(10_000_000) + .build().unwrap(); + + let payment_id = PaymentId([1; 32]); + bob.node.pay_for_offer(&offer, None, payment_id, Default::default()).unwrap(); + expect_recent_payment!(bob, RecentPaymentDetails::AwaitingInvoice, payment_id); + + // Bob -> Alice: invoice_request. When Alice handles it, her flow asks the router for blinded + // payment paths; the router applies the configured metadata override before the path is built + // and embedded in the invoice. + let onion_message = bob.onion_messenger.next_onion_message_for_peer(alice_id).unwrap(); + alice.onion_messenger.handle_onion_message(bob_id, &onion_message); + + let (invoice_request, _) = extract_invoice_request(alice, &onion_message); + + // Alice -> Bob: invoice (carrying the blinded path with the modified payment_context). + 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); + + let payment_context = PaymentContext::Bolt12Offer(Bolt12OfferContext { + offer_id: offer.id(), + invoice_request: InvoiceRequestFields { + payer_signing_pubkey: invoice_request.payer_signing_pubkey(), + quantity: None, + payer_note_truncated: None, + human_readable_name: None, + }, + payment_metadata: Some(expected_metadata), + }); + + route_bolt12_payment(bob, &[alice], &invoice); + expect_recent_payment!(bob, RecentPaymentDetails::Pending, payment_id); + + // Verifies that Alice's `Event::PaymentClaimable` carries the `payment_metadata` injected by + // the router (via the `expected_payment_context` equality check inside this helper). + claim_bolt12_payment(bob, &[alice], payment_context, &invoice); + expect_recent_payment!(bob, RecentPaymentDetails::Fulfilled, payment_id); +} + +/// Checks that `payment_metadata` set in the [`OffersContext::InvoiceRequest`] of an offer's +/// blinded message path is propagated to the [`Bolt12OfferContext`] in the resulting invoice's +/// blinded payment paths and surfaced via [`Event::PaymentClaimable`] when the payment is received. +#[test] +fn pays_for_offer_with_payment_metadata_in_invoice_request_context() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 10_000_000, 1_000_000_000); + + let alice = &nodes[0]; + let alice_id = alice.node.get_our_node_id(); + let bob = &nodes[1]; + let bob_id = bob.node.get_our_node_id(); + + // Manually build an offer whose blinded message path carries `payment_metadata` in its + // `OffersContext::InvoiceRequest` context. The HEAD commit causes Alice's `ChannelManager` to + // copy this metadata onto the `Bolt12OfferContext` when she handles the inbound invoice + // request, embedding it in the invoice's blinded payment paths. + let mut expected_metadata = BTreeMap::new(); + expected_metadata.insert(0u64, vec![1, 2, 3, 4]); + expected_metadata.insert(7u64, vec![0xab, 0xcd]); + + let secp_ctx = Secp256k1::new(); + let nonce = Nonce::from_entropy_source(alice.keys_manager); + let context = MessageContext::Offers(OffersContext::InvoiceRequest { + nonce, + payment_metadata: Some(expected_metadata.clone()), + }); + let paths = alice.message_router.create_blinded_paths( + alice_id, + alice.keys_manager.get_receive_auth_key(), + context, + alice.node.test_get_peers_for_blinded_path(), + &secp_ctx, + ).unwrap(); + assert!(!paths.is_empty()); + + let expanded_key = alice.keys_manager.get_expanded_key(); + let mut builder = OfferBuilder::deriving_signing_pubkey(alice_id, &expanded_key, nonce, &secp_ctx) + .chain(Network::Testnet) + .amount_msats(10_000_000); + for path in paths { + builder = builder.path(path); + } + let offer = builder.build().unwrap(); + + let payment_id = PaymentId([1; 32]); + bob.node.pay_for_offer(&offer, None, payment_id, Default::default()).unwrap(); + expect_recent_payment!(bob, RecentPaymentDetails::AwaitingInvoice, payment_id); + + let onion_message = bob.onion_messenger.next_onion_message_for_peer(alice_id).unwrap(); + alice.onion_messenger.handle_onion_message(bob_id, &onion_message); + + let (invoice_request, _) = extract_invoice_request(alice, &onion_message); + + 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); + + let payment_context = PaymentContext::Bolt12Offer(Bolt12OfferContext { + offer_id: offer.id(), + invoice_request: InvoiceRequestFields { + payer_signing_pubkey: invoice_request.payer_signing_pubkey(), + quantity: None, + payer_note_truncated: None, + human_readable_name: None, + }, + payment_metadata: Some(expected_metadata), + }); + + route_bolt12_payment(bob, &[alice], &invoice); + expect_recent_payment!(bob, RecentPaymentDetails::Pending, payment_id); + + // `claim_bolt12_payment` asserts the surfaced `PaymentContext` matches `payment_context` + // above, including the embedded `payment_metadata`. + claim_bolt12_payment(bob, &[alice], payment_context, &invoice); + expect_recent_payment!(bob, RecentPaymentDetails::Fulfilled, payment_id); +} + /// Checks that a refund can be paid through a one-hop blinded path and that ephemeral pubkeys are /// used rather than exposing a node's pubkey. However, the node's pubkey is still used as the /// introduction node of the blinded path. @@ -942,7 +1099,7 @@ fn creates_and_pays_for_refund_using_one_hop_blinded_path() { } expect_recent_payment!(bob, RecentPaymentDetails::AwaitingInvoice, payment_id); - let payment_context = PaymentContext::Bolt12Refund(Bolt12RefundContext {}); + let payment_context = PaymentContext::Bolt12Refund(Bolt12RefundContext { payment_metadata: None }); let expected_invoice = alice.node.request_refund_payment(&refund).unwrap(); let onion_message = alice.onion_messenger.next_onion_message_for_peer(bob_id).unwrap(); @@ -1007,6 +1164,7 @@ fn pays_for_offer_without_blinded_paths() { payer_note_truncated: None, human_readable_name: None, }, + payment_metadata: None, }); let onion_message = alice.onion_messenger.next_onion_message_for_peer(bob_id).unwrap(); @@ -1047,7 +1205,7 @@ fn pays_for_refund_without_blinded_paths() { assert!(refund.paths().is_empty()); expect_recent_payment!(bob, RecentPaymentDetails::AwaitingInvoice, payment_id); - let payment_context = PaymentContext::Bolt12Refund(Bolt12RefundContext {}); + let payment_context = PaymentContext::Bolt12Refund(Bolt12RefundContext { payment_metadata: None }); let expected_invoice = alice.node.request_refund_payment(&refund).unwrap(); let onion_message = alice.onion_messenger.next_onion_message_for_peer(bob_id).unwrap(); @@ -1275,6 +1433,7 @@ fn creates_and_pays_for_offer_with_retry() { payer_note_truncated: None, human_readable_name: None, }, + payment_metadata: None, }); assert_eq!(invoice_request.amount_msats(), Some(10_000_000)); assert_ne!(invoice_request.payer_signing_pubkey(), bob_id); @@ -1340,6 +1499,7 @@ fn pays_bolt12_invoice_asynchronously() { payer_note_truncated: None, human_readable_name: None, }, + payment_metadata: None, }); let onion_message = alice.onion_messenger.next_onion_message_for_peer(bob_id).unwrap(); @@ -1437,6 +1597,7 @@ fn creates_offer_with_blinded_path_using_unannounced_introduction_node() { payer_note_truncated: None, human_readable_name: None, }, + payment_metadata: None, }); assert_ne!(invoice_request.payer_signing_pubkey(), bob_id); assert_eq!(reply_path.introduction_node(), &IntroductionNode::NodeId(alice_id)); @@ -2280,7 +2441,7 @@ fn fails_paying_invoice_more_than_once() { david.onion_messenger.handle_onion_message(charlie_id, &onion_message); // David initiates paying the first invoice - let payment_context = PaymentContext::Bolt12Refund(Bolt12RefundContext {}); + let payment_context = PaymentContext::Bolt12Refund(Bolt12RefundContext { payment_metadata: None }); let (invoice1, _) = extract_invoice(david, &onion_message); route_bolt12_payment(david, &[charlie, bob, alice], &invoice1); @@ -2648,6 +2809,7 @@ fn creates_and_pays_for_phantom_offer() { payer_note_truncated: None, human_readable_name: None, }, + payment_metadata: None, }); let onion_message = diff --git a/lightning/src/offers/flow.rs b/lightning/src/offers/flow.rs index 6c1b7a5befe..bdc3475b554 100644 --- a/lightning/src/offers/flow.rs +++ b/lightning/src/offers/flow.rs @@ -10,6 +10,8 @@ //! Provides data structures and functions for creating and managing Offers messages, //! facilitating communication, and handling BOLT12 messages and payments. +use alloc::collections::BTreeMap; + use core::sync::atomic::{AtomicUsize, Ordering}; use core::time::Duration; @@ -452,7 +454,7 @@ impl OffersMessageFlow { let nonce = match context { None if invoice_request.metadata().is_some() => None, - Some(OffersContext::InvoiceRequest { nonce }) => Some(nonce), + Some(OffersContext::InvoiceRequest { nonce, payment_metadata: _ }) => Some(nonce), Some(OffersContext::StaticInvoiceRequested { recipient_id, invoice_slot, @@ -559,7 +561,8 @@ impl OffersMessageFlow { let secp_ctx = &self.secp_ctx; let nonce = Nonce::from_entropy_source(entropy); - let context = MessageContext::Offers(OffersContext::InvoiceRequest { nonce }); + let context = + MessageContext::Offers(OffersContext::InvoiceRequest { nonce, payment_metadata: None }); let mut builder = OfferBuilder::deriving_signing_pubkey(node_id, expanded_key, nonce, secp_ctx) @@ -828,13 +831,15 @@ impl OffersMessageFlow { pub fn create_static_invoice_builder<'a, R: Router>( &self, router: &R, offer: &'a Offer, offer_nonce: Nonce, payment_secret: PaymentSecret, relative_expiry_secs: u32, usable_channels: Vec, - peers: Vec, + peers: Vec, payment_metadata: Option>>, ) -> Result, Bolt12SemanticError> { let expanded_key = &self.inbound_payment_key; let secp_ctx = &self.secp_ctx; - let payment_context = - PaymentContext::AsyncBolt12Offer(AsyncBolt12OfferContext { offer_nonce }); + let payment_context = PaymentContext::AsyncBolt12Offer(AsyncBolt12OfferContext { + offer_nonce, + payment_metadata, + }); let amount_msat = offer.amount().and_then(|amount| match amount { Amount::Bitcoin { amount_msats } => Some(amount_msats), @@ -896,6 +901,7 @@ impl OffersMessageFlow { pub fn create_invoice_builder_from_refund<'a, ES: EntropySource, R: Router, F>( &'a self, router: &R, entropy_source: ES, refund: &'a Refund, usable_channels: Vec, get_payment_info: F, + payment_metadata: Option>>, ) -> Result, Bolt12SemanticError> where F: Fn(u64, u32) -> Result<(PaymentHash, PaymentSecret), Bolt12SemanticError>, @@ -912,7 +918,8 @@ impl OffersMessageFlow { let (payment_hash, payment_secret) = get_payment_info(amount_msats, relative_expiry)?; - let payment_context = PaymentContext::Bolt12Refund(Bolt12RefundContext {}); + let payment_context = + PaymentContext::Bolt12Refund(Bolt12RefundContext { payment_metadata }); let payment_paths = self .create_blinded_payment_paths( router, @@ -963,6 +970,7 @@ impl OffersMessageFlow { pub fn create_invoice_builder_from_invoice_request_with_keys<'a, R: Router, F>( &self, router: &R, invoice_request: &'a VerifiedInvoiceRequest, usable_channels: Vec, get_payment_info: F, + payment_metadata: Option>>, ) -> Result<(InvoiceBuilder<'a, DerivedSigningPubkey>, MessageContext), Bolt12SemanticError> where F: Fn(u64, u32) -> Result<(PaymentHash, PaymentSecret), Bolt12SemanticError>, @@ -977,6 +985,7 @@ impl OffersMessageFlow { let context = PaymentContext::Bolt12Offer(Bolt12OfferContext { offer_id: invoice_request.offer_id, invoice_request: invoice_request.fields(), + payment_metadata, }); let payment_paths = self @@ -1022,6 +1031,7 @@ impl OffersMessageFlow { pub fn create_invoice_builder_from_invoice_request_without_keys<'a, R: Router, F>( &self, router: &R, invoice_request: &'a VerifiedInvoiceRequest, usable_channels: Vec, get_payment_info: F, + payment_metadata: Option>>, ) -> Result<(InvoiceBuilder<'a, ExplicitSigningPubkey>, MessageContext), Bolt12SemanticError> where F: Fn(u64, u32) -> Result<(PaymentHash, PaymentSecret), Bolt12SemanticError>, @@ -1036,6 +1046,7 @@ impl OffersMessageFlow { let context = PaymentContext::Bolt12Offer(Bolt12OfferContext { offer_id: invoice_request.offer_id, invoice_request: invoice_request.fields(), + payment_metadata, }); let payment_paths = self @@ -1643,11 +1654,15 @@ impl OffersMessageFlow { offer_relative_expiry, usable_channels, peers.clone(), + None, ) .and_then(|builder| builder.build_and_sign(secp_ctx)) .map_err(|_| ())?; - let context = MessageContext::Offers(OffersContext::InvoiceRequest { nonce: offer_nonce }); + let context = MessageContext::Offers(OffersContext::InvoiceRequest { + nonce: offer_nonce, + payment_metadata: None, + }); let forward_invoice_request_path = self .create_blinded_paths(peers, context) .and_then(|paths| paths.into_iter().next().ok_or(()))?; diff --git a/lightning/src/onion_message/messenger.rs b/lightning/src/onion_message/messenger.rs index 7ef4e4a66a8..98a54e21b17 100644 --- a/lightning/src/onion_message/messenger.rs +++ b/lightning/src/onion_message/messenger.rs @@ -469,6 +469,11 @@ pub trait MessageRouter { /// Creates [`BlindedMessagePath`]s to the `recipient` node. The nodes in `peers` are assumed to /// be direct peers with the `recipient`. + /// + /// While payments will fail if most of `context` is modified, modifying + /// [`OffersContext::InvoiceRequest::payment_metadata`] prior to blinded path construction is + /// allowed. + /// fn create_blinded_paths( &self, recipient: PublicKey, local_node_receive_key: ReceiveAuthKey, context: MessageContext, peers: Vec, secp_ctx: &Secp256k1, diff --git a/lightning/src/routing/router.rs b/lightning/src/routing/router.rs index edb048c8c7d..f7da1855120 100644 --- a/lightning/src/routing/router.rs +++ b/lightning/src/routing/router.rs @@ -283,6 +283,12 @@ pub trait Router { /// Creates [`BlindedPaymentPath`]s for payment to the `recipient` node. The channels in `first_hops` /// are assumed to be with the `recipient`'s peers. The payment secret and any constraints are /// given in `tlvs`. The `local_node_receive_key` is required to authenticate the blinded payment paths. + /// + /// While payments will fail if most of `tlvs` is modified, modifying + /// [`ReceiveTlvs::payment_context`]'s [`PaymentContext::payment_metadata`] fields prior to + /// blinded path construction is allowed. + /// + /// [`PaymentContext::payment_metadata`]: crate::blinded_path::payment::PaymentContext::payment_metadata fn create_blinded_payment_paths( &self, recipient: PublicKey, local_node_receive_key: ReceiveAuthKey, first_hops: Vec, tlvs: ReceiveTlvs, amount_msats: Option, diff --git a/lightning/src/util/ser.rs b/lightning/src/util/ser.rs index bd2488bd8d1..0f93df22cd2 100644 --- a/lightning/src/util/ser.rs +++ b/lightning/src/util/ser.rs @@ -610,6 +610,13 @@ macro_rules! impl_writeable_primitive { writer.write_all(&self.0.to_be_bytes()[(self.0.leading_zeros() / 8) as usize..$len]) } } + impl Writeable for HighZeroBytesDroppedBigSize<&$val_type> { + #[inline] + fn write(&self, writer: &mut W) -> Result<(), io::Error> { + // Skip any full leading 0 bytes when writing (in BE): + writer.write_all(&self.0.to_be_bytes()[(self.0.leading_zeros() / 8) as usize..$len]) + } + } impl Readable for $val_type { #[inline] fn read(reader: &mut R) -> Result<$val_type, DecodeError> { @@ -751,12 +758,20 @@ impl_array!(HMAC_LEN * HMAC_COUNT, u8); /// This is not exported to bindings users as manual TLV building is not currently supported in bindings pub struct WithoutLength(pub T); +impl Writeable for WithoutLength<&&String> { + #[inline] + fn write(&self, w: &mut W) -> Result<(), io::Error> { + w.write_all(self.0.as_bytes()) + } +} + impl Writeable for WithoutLength<&String> { #[inline] fn write(&self, w: &mut W) -> Result<(), io::Error> { w.write_all(self.0.as_bytes()) } } + impl LengthReadable for WithoutLength { #[inline] fn read_from_fixed_length_buffer(r: &mut R) -> Result { @@ -808,6 +823,14 @@ impl AsWriteableSlice for &Vec { &self } } + +impl AsWriteableSlice for &&Vec { + type Inner = T; + fn as_slice(&self) -> &[T] { + &self + } +} + impl AsWriteableSlice for &[T] { type Inner = T; fn as_slice(&self) -> &[T] { @@ -946,6 +969,37 @@ macro_rules! impl_for_map { impl_for_map!(BTreeMap, Ord, |_| BTreeMap::new()); impl_for_map!(HashMap, Hash, |len| hash_map_with_capacity(len)); +/// A wrapper used to serialize a `BTreeMap>` with a few less bytes. +pub(crate) struct BigSizeKeyedMap(pub T); + +impl Writeable for BigSizeKeyedMap<&BTreeMap>> { + #[inline] + fn write(&self, w: &mut W) -> Result<(), io::Error> { + BigSize(self.0.len() as u64).write(w)?; + for (key, value) in self.0.iter() { + BigSize(*key).write(w)?; + value.write(w)?; + } + Ok(()) + } +} + +impl LengthReadable for BigSizeKeyedMap>> { + #[inline] + fn read_from_fixed_length_buffer(r: &mut R) -> Result { + let len: BigSize = Readable::read(r)?; + let mut ret = BTreeMap::new(); + for _ in 0..len.0 { + let key: BigSize = Readable::read(r)?; + let value: Vec = Readable::read(r)?; + if ret.insert(key.0, value).is_some() { + return Err(DecodeError::InvalidValue); + } + } + Ok(BigSizeKeyedMap(ret)) + } +} + // HashSet impl Writeable for HashSet where diff --git a/lightning/src/util/ser_macros.rs b/lightning/src/util/ser_macros.rs index 946be54de65..53777d26130 100644 --- a/lightning/src/util/ser_macros.rs +++ b/lightning/src/util/ser_macros.rs @@ -80,8 +80,8 @@ macro_rules! _encode_tlv { ($stream: expr, $type: expr, $field: expr, upgradable_option $(, $self: ident)?) => { $crate::_encode_tlv!($stream, $type, $field, option); }; - ($stream: expr, $type: expr, $field: expr, (option, encoding: ($fieldty: ty, $encoding: ident) $(, $self: ident)?)) => { - $crate::_encode_tlv!($stream, $type, $field.map(|f| $encoding(f)), option); + ($stream: expr, $type: expr, $field: expr, (option, encoding: ($fieldty: ty, $encoding: ident)) $(, $self: ident)?) => { + $crate::_encode_tlv!($stream, $type, $field.as_ref().map(|f| $encoding(f)), option); }; ($stream: expr, $type: expr, $field: expr, (option, encoding: $fieldty: ty) $(, $self: ident)?) => { $crate::_encode_tlv!($stream, $type, $field, option); @@ -253,8 +253,7 @@ macro_rules! _get_varint_length_prefixed_tlv_length { $crate::_get_varint_length_prefixed_tlv_length!($len, $type, $field, option); }; ($len: expr, $type: expr, $field: expr, (option, encoding: ($fieldty: ty, $encoding: ident)) $(, $self: ident)?) => { - let field = $field.map(|f| $encoding(f)); - $crate::_get_varint_length_prefixed_tlv_length!($len, $type, field, option); + $crate::_get_varint_length_prefixed_tlv_length!($len, $type, $field.as_ref().map(|f| $encoding(f)), option); }; ($len: expr, $type: expr, $field: expr, upgradable_required $(, $self: ident)?) => { $crate::_get_varint_length_prefixed_tlv_length!($len, $type, $field, required); diff --git a/lightning/src/util/test_utils.rs b/lightning/src/util/test_utils.rs index d7320ff2ba9..892c9f4169d 100644 --- a/lightning/src/util/test_utils.rs +++ b/lightning/src/util/test_utils.rs @@ -7,9 +7,11 @@ // You may not use this file except in accordance with one or both of these // licenses. +use alloc::collections::BTreeMap; + use crate::blinded_path::message::MessageContext; use crate::blinded_path::message::{BlindedMessagePath, MessageForwardNode}; -use crate::blinded_path::payment::{BlindedPaymentPath, ReceiveTlvs}; +use crate::blinded_path::payment::{BlindedPaymentPath, PaymentContext, ReceiveTlvs}; use crate::chain; use crate::chain::chaininterface; #[cfg(any(test, feature = "_externalize_tests"))] @@ -178,6 +180,7 @@ pub struct TestRouter<'a> { pub network_graph: Arc>, pub next_routes: Mutex>)>>, pub next_blinded_payment_paths: Mutex>, + pub next_payment_context_metadata: Mutex>>>, pub scorer: &'a RwLock, } @@ -189,6 +192,7 @@ impl<'a> TestRouter<'a> { let entropy_source = Arc::new(RandomBytes::new([42; 32])); let next_routes = Mutex::new(VecDeque::new()); let next_blinded_payment_paths = Mutex::new(Vec::new()); + let next_payment_context_metadata = Mutex::new(None); Self { router: DefaultRouter::new( Arc::clone(&network_graph), @@ -200,10 +204,15 @@ impl<'a> TestRouter<'a> { network_graph, next_routes, next_blinded_payment_paths, + next_payment_context_metadata, scorer, } } + pub fn set_next_payment_context_metadata(&self, metadata: BTreeMap>) { + *self.next_payment_context_metadata.lock().unwrap() = Some(metadata); + } + pub fn expect_find_route(&self, query: RouteParameters, result: Result) { let mut expected_routes = self.next_routes.lock().unwrap(); expected_routes.push_back((query, Some(result))); @@ -319,9 +328,16 @@ impl<'a> Router for TestRouter<'a> { fn create_blinded_payment_paths( &self, recipient: PublicKey, local_node_receive_key: ReceiveAuthKey, - first_hops: Vec, tlvs: ReceiveTlvs, amount_msats: Option, + first_hops: Vec, mut tlvs: ReceiveTlvs, amount_msats: Option, secp_ctx: &Secp256k1, ) -> Result, ()> { + if let Some(metadata) = self.next_payment_context_metadata.lock().unwrap().take() { + match &mut tlvs.payment_context { + PaymentContext::Bolt12Offer(ctx) => ctx.payment_metadata = Some(metadata), + PaymentContext::AsyncBolt12Offer(ctx) => ctx.payment_metadata = Some(metadata), + PaymentContext::Bolt12Refund(ctx) => ctx.payment_metadata = Some(metadata), + } + } let mut expected_paths = self.next_blinded_payment_paths.lock().unwrap(); if expected_paths.is_empty() { self.router.create_blinded_payment_paths(