Skip to content

Commit ee6ba89

Browse files
committed
Add a payment_metadata map in BOLT 12 blinded message path ctxs
Similar to how BOLT 11 payments can use a `payment_metadata` to provide arbitrary bytes in the invoice to be communicated back to them when receiving, its useful to be able to provide some bytes which are communicated back upon receiving a payment. Here we do so in the BOLT 12 blinded message path contexts, offering a `BTreeMap<u64, Vec<u8>>` instead to enable more easily including multiple sets of data. We don't yet wire it up to the public `ChannelManager` API, but do allow selecting values for those using the manual `OffersMessageFlow`. Tests by claude
1 parent 4499708 commit ee6ba89

6 files changed

Lines changed: 134 additions & 12 deletions

File tree

lightning/src/blinded_path/message.rs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99

1010
//! Data structures and methods for constructing [`BlindedMessagePath`]s to send a message over.
1111
12+
use alloc::collections::BTreeMap;
13+
1214
use bitcoin::secp256k1::{self, PublicKey, Secp256k1, SecretKey};
1315

1416
#[allow(unused_imports)]
@@ -29,7 +31,9 @@ use crate::routing::gossip::{NodeId, ReadOnlyNetworkGraph};
2931
use crate::sign::{EntropySource, NodeSigner, ReceiveAuthKey, Recipient};
3032
use crate::types::payment::PaymentHash;
3133
use crate::util::scid_utils;
32-
use crate::util::ser::{FixedLengthReader, LengthReadableArgs, Readable, Writeable, Writer};
34+
use crate::util::ser::{
35+
BigSizeKeyedMap, FixedLengthReader, LengthReadableArgs, Readable, Writeable, Writer,
36+
};
3337

3438
use core::time::Duration;
3539
use core::{cmp, mem};
@@ -391,6 +395,23 @@ pub enum OffersContext {
391395
/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
392396
/// [`Offer`]: crate::offers::offer::Offer
393397
nonce: Nonce,
398+
399+
/// Additional data about this payment which is not used in LDK and can be used for any
400+
/// purpose.
401+
///
402+
/// This is analogous to the BOLT 11 [`RecipientOnionFields::payment_metadata`] (which is
403+
/// provided to payers via [`Bolt11Invoice::payment_metadata`]) and can be used any time data
404+
/// needs to be "stored" by a payment recipient for their own internal use, provided back to
405+
/// them with the payment.
406+
///
407+
/// Note that because this is included in the payment onion, its size must be tighly
408+
/// constrained. More than a few hundred bytes and the payment will be entirely unpayable (with
409+
/// limited routing options as size increases). Further, any data placed here will increase
410+
/// the size of the offer which may make it difficult to fit in QR codes.
411+
///
412+
/// [`RecipientOnionFields::payment_metadata`]: crate::ln::outbound_payment::RecipientOnionFields::payment_metadata
413+
/// [`Bolt11Invoice::payment_metadata`]: lightning_invoice::Bolt11Invoice::payment_metadata
414+
payment_metadata: Option<BTreeMap<u64, Vec<u8>>>,
394415
},
395416
/// Context used by a [`BlindedMessagePath`] within the [`Offer`] of an async recipient.
396417
///
@@ -648,6 +669,7 @@ impl_writeable_tlv_based_enum!(MessageContext,
648669
impl_writeable_tlv_based_enum!(OffersContext,
649670
(0, InvoiceRequest) => {
650671
(0, nonce, required),
672+
(1, payment_metadata, (option, encoding: (BTreeMap<u64, Vec<u8>>, BigSizeKeyedMap))),
651673
},
652674
(1, OutboundPaymentForRefund) => {
653675
(0, payment_id, required),

lightning/src/ln/async_payments_tests.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,7 @@ fn create_static_invoice<T: secp256k1::Signing + secp256k1::Verification>(
317317
.create_blinded_paths(
318318
always_online_counterparty.node.get_our_node_id(),
319319
always_online_counterparty.keys_manager.get_receive_auth_key(),
320-
MessageContext::Offers(OffersContext::InvoiceRequest { nonce: Nonce([42; 16]) }),
320+
MessageContext::Offers(OffersContext::InvoiceRequest { nonce: Nonce([42; 16]), payment_metadata: None }),
321321
Vec::new(),
322322
&secp_ctx,
323323
)
@@ -688,7 +688,7 @@ fn static_invoice_unknown_required_features() {
688688
.create_blinded_paths(
689689
nodes[1].node.get_our_node_id(),
690690
nodes[1].keys_manager.get_receive_auth_key(),
691-
MessageContext::Offers(OffersContext::InvoiceRequest { nonce: Nonce([42; 16]) }),
691+
MessageContext::Offers(OffersContext::InvoiceRequest { nonce: Nonce([42; 16]), payment_metadata: None }),
692692
Vec::new(),
693693
&secp_ctx,
694694
)
@@ -1755,7 +1755,7 @@ fn invalid_async_receive_with_retry<F1, F2>(
17551755
.create_blinded_paths(
17561756
nodes[1].node.get_our_node_id(),
17571757
nodes[1].keys_manager.get_receive_auth_key(),
1758-
MessageContext::Offers(OffersContext::InvoiceRequest { nonce: Nonce([42; 16]) }),
1758+
MessageContext::Offers(OffersContext::InvoiceRequest { nonce: Nonce([42; 16]), payment_metadata: None }),
17591759
Vec::new(),
17601760
&secp_ctx,
17611761
)

lightning/src/ln/channelmanager.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17092,6 +17092,13 @@ impl<
1709217092
None => return None,
1709317093
};
1709417094

17095+
let payment_metadata =
17096+
if let Some(OffersContext::InvoiceRequest { payment_metadata, .. }) = &context {
17097+
payment_metadata.clone()
17098+
} else {
17099+
None
17100+
};
17101+
1709517102
let invoice_request = match self.flow.verify_invoice_request(invoice_request, context) {
1709617103
Ok(InvreqResponseInstructions::SendInvoice(invoice_request)) => invoice_request,
1709717104
Ok(InvreqResponseInstructions::SendStaticInvoice { recipient_id, invoice_slot, invoice_request }) => {
@@ -17119,7 +17126,7 @@ impl<
1711917126
&request,
1712017127
self.list_usable_channels(),
1712117128
get_payment_info,
17122-
None,
17129+
payment_metadata,
1712317130
);
1712417131

1712517132
match result {
@@ -17144,7 +17151,7 @@ impl<
1714417151
&request,
1714517152
self.list_usable_channels(),
1714617153
get_payment_info,
17147-
None,
17154+
payment_metadata,
1714817155
);
1714917156

1715017157
match result {

lightning/src/ln/offers_tests.rs

Lines changed: 87 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ use core::time::Duration;
5050
use crate::blinded_path::IntroductionNode;
5151
use crate::blinded_path::message::BlindedMessagePath;
5252
use crate::blinded_path::payment::{Bolt12OfferContext, Bolt12RefundContext, DummyTlvs, PaymentContext};
53-
use crate::blinded_path::message::OffersContext;
53+
use crate::blinded_path::message::{MessageContext, OffersContext};
5454
use crate::events::{ClosureReason, Event, HTLCHandlingFailureType, PaidBolt12Invoice, PaymentFailureReason, PaymentPurpose};
5555
use crate::ln::channelmanager::{PaymentId, RecentPaymentDetails, self};
5656
use crate::ln::outbound_payment::{Bolt12PaymentError, RecipientOnionFields, Retry};
@@ -62,8 +62,9 @@ use crate::offers::invoice::Bolt12Invoice;
6262
use crate::offers::invoice_error::InvoiceError;
6363
use crate::offers::invoice_request::{InvoiceRequest, InvoiceRequestFields, InvoiceRequestVerifiedFromOffer};
6464
use crate::offers::nonce::Nonce;
65+
use crate::offers::offer::OfferBuilder;
6566
use crate::offers::parse::Bolt12SemanticError;
66-
use crate::onion_message::messenger::{DefaultMessageRouter, Destination, MessageSendInstructions, NodeIdMessageRouter, NullMessageRouter, PeeledOnion, DUMMY_HOPS_PATH_LENGTH, QR_CODED_DUMMY_HOPS_PATH_LENGTH};
67+
use crate::onion_message::messenger::{DefaultMessageRouter, Destination, MessageRouter, MessageSendInstructions, NodeIdMessageRouter, NullMessageRouter, PeeledOnion, DUMMY_HOPS_PATH_LENGTH, QR_CODED_DUMMY_HOPS_PATH_LENGTH};
6768
use crate::onion_message::offers::OffersMessage;
6869
use crate::routing::gossip::{NodeAlias, NodeId};
6970
use crate::routing::router::{DEFAULT_PAYMENT_DUMMY_HOPS, PaymentParameters, RouteParameters, RouteParametersConfig};
@@ -258,7 +259,7 @@ fn claim_bolt12_payment_with_extra_fees<'a, 'b, 'c>(
258259

259260
fn extract_offer_nonce<'a, 'b, 'c>(node: &Node<'a, 'b, 'c>, message: &OnionMessage) -> Nonce {
260261
match node.onion_messenger.peel_onion_message(message) {
261-
Ok(PeeledOnion::Offers(_, Some(OffersContext::InvoiceRequest { nonce }), _)) => nonce,
262+
Ok(PeeledOnion::Offers(_, Some(OffersContext::InvoiceRequest { nonce, payment_metadata: _ }), _)) => nonce,
262263
Ok(PeeledOnion::Offers(_, context, _)) => panic!("Unexpected onion message context: {:?}", context),
263264
Ok(PeeledOnion::Forward(_, _)) => panic!("Unexpected onion message forward"),
264265
Ok(_) => panic!("Unexpected onion message"),
@@ -983,6 +984,89 @@ fn router_modifies_payment_metadata_in_blinded_path() {
983984
expect_recent_payment!(bob, RecentPaymentDetails::Fulfilled, payment_id);
984985
}
985986

987+
/// Checks that `payment_metadata` set in the [`OffersContext::InvoiceRequest`] of an offer's
988+
/// blinded message path is propagated to the [`Bolt12OfferContext`] in the resulting invoice's
989+
/// blinded payment paths and surfaced via [`Event::PaymentClaimable`] when the payment is received.
990+
#[test]
991+
fn pays_for_offer_with_payment_metadata_in_invoice_request_context() {
992+
let chanmon_cfgs = create_chanmon_cfgs(2);
993+
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
994+
let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]);
995+
let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
996+
997+
create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 10_000_000, 1_000_000_000);
998+
999+
let alice = &nodes[0];
1000+
let alice_id = alice.node.get_our_node_id();
1001+
let bob = &nodes[1];
1002+
let bob_id = bob.node.get_our_node_id();
1003+
1004+
// Manually build an offer whose blinded message path carries `payment_metadata` in its
1005+
// `OffersContext::InvoiceRequest` context. The HEAD commit causes Alice's `ChannelManager` to
1006+
// copy this metadata onto the `Bolt12OfferContext` when she handles the inbound invoice
1007+
// request, embedding it in the invoice's blinded payment paths.
1008+
let mut expected_metadata = BTreeMap::new();
1009+
expected_metadata.insert(0u64, vec![1, 2, 3, 4]);
1010+
expected_metadata.insert(7u64, vec![0xab, 0xcd]);
1011+
1012+
let secp_ctx = Secp256k1::new();
1013+
let nonce = Nonce::from_entropy_source(alice.keys_manager);
1014+
let context = MessageContext::Offers(OffersContext::InvoiceRequest {
1015+
nonce,
1016+
payment_metadata: Some(expected_metadata.clone()),
1017+
});
1018+
let paths = alice.message_router.create_blinded_paths(
1019+
alice_id,
1020+
alice.keys_manager.get_receive_auth_key(),
1021+
context,
1022+
alice.node.test_get_peers_for_blinded_path(),
1023+
&secp_ctx,
1024+
).unwrap();
1025+
assert!(!paths.is_empty());
1026+
1027+
let expanded_key = alice.keys_manager.get_expanded_key();
1028+
let mut builder = OfferBuilder::deriving_signing_pubkey(alice_id, &expanded_key, nonce, &secp_ctx)
1029+
.chain(Network::Testnet)
1030+
.amount_msats(10_000_000);
1031+
for path in paths {
1032+
builder = builder.path(path);
1033+
}
1034+
let offer = builder.build().unwrap();
1035+
1036+
let payment_id = PaymentId([1; 32]);
1037+
bob.node.pay_for_offer(&offer, None, payment_id, Default::default()).unwrap();
1038+
expect_recent_payment!(bob, RecentPaymentDetails::AwaitingInvoice, payment_id);
1039+
1040+
let onion_message = bob.onion_messenger.next_onion_message_for_peer(alice_id).unwrap();
1041+
alice.onion_messenger.handle_onion_message(bob_id, &onion_message);
1042+
1043+
let (invoice_request, _) = extract_invoice_request(alice, &onion_message);
1044+
1045+
let onion_message = alice.onion_messenger.next_onion_message_for_peer(bob_id).unwrap();
1046+
bob.onion_messenger.handle_onion_message(alice_id, &onion_message);
1047+
1048+
let (invoice, _) = extract_invoice(bob, &onion_message);
1049+
1050+
let payment_context = PaymentContext::Bolt12Offer(Bolt12OfferContext {
1051+
offer_id: offer.id(),
1052+
invoice_request: InvoiceRequestFields {
1053+
payer_signing_pubkey: invoice_request.payer_signing_pubkey(),
1054+
quantity: None,
1055+
payer_note_truncated: None,
1056+
human_readable_name: None,
1057+
},
1058+
payment_metadata: Some(expected_metadata),
1059+
});
1060+
1061+
route_bolt12_payment(bob, &[alice], &invoice);
1062+
expect_recent_payment!(bob, RecentPaymentDetails::Pending, payment_id);
1063+
1064+
// `claim_bolt12_payment` asserts the surfaced `PaymentContext` matches `payment_context`
1065+
// above, including the embedded `payment_metadata`.
1066+
claim_bolt12_payment(bob, &[alice], payment_context, &invoice);
1067+
expect_recent_payment!(bob, RecentPaymentDetails::Fulfilled, payment_id);
1068+
}
1069+
9861070
/// Checks that a refund can be paid through a one-hop blinded path and that ephemeral pubkeys are
9871071
/// used rather than exposing a node's pubkey. However, the node's pubkey is still used as the
9881072
/// introduction node of the blinded path.

lightning/src/offers/flow.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -454,7 +454,7 @@ impl<MR: MessageRouter, L: Logger> OffersMessageFlow<MR, L> {
454454

455455
let nonce = match context {
456456
None if invoice_request.metadata().is_some() => None,
457-
Some(OffersContext::InvoiceRequest { nonce }) => Some(nonce),
457+
Some(OffersContext::InvoiceRequest { nonce, payment_metadata: _ }) => Some(nonce),
458458
Some(OffersContext::StaticInvoiceRequested {
459459
recipient_id,
460460
invoice_slot,
@@ -561,7 +561,8 @@ impl<MR: MessageRouter, L: Logger> OffersMessageFlow<MR, L> {
561561
let secp_ctx = &self.secp_ctx;
562562

563563
let nonce = Nonce::from_entropy_source(entropy);
564-
let context = MessageContext::Offers(OffersContext::InvoiceRequest { nonce });
564+
let context =
565+
MessageContext::Offers(OffersContext::InvoiceRequest { nonce, payment_metadata: None });
565566

566567
let mut builder =
567568
OfferBuilder::deriving_signing_pubkey(node_id, expanded_key, nonce, secp_ctx)
@@ -1658,7 +1659,10 @@ impl<MR: MessageRouter, L: Logger> OffersMessageFlow<MR, L> {
16581659
.and_then(|builder| builder.build_and_sign(secp_ctx))
16591660
.map_err(|_| ())?;
16601661

1661-
let context = MessageContext::Offers(OffersContext::InvoiceRequest { nonce: offer_nonce });
1662+
let context = MessageContext::Offers(OffersContext::InvoiceRequest {
1663+
nonce: offer_nonce,
1664+
payment_metadata: None,
1665+
});
16621666
let forward_invoice_request_path = self
16631667
.create_blinded_paths(peers, context)
16641668
.and_then(|paths| paths.into_iter().next().ok_or(()))?;

lightning/src/onion_message/messenger.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,11 @@ pub trait MessageRouter {
469469

470470
/// Creates [`BlindedMessagePath`]s to the `recipient` node. The nodes in `peers` are assumed to
471471
/// be direct peers with the `recipient`.
472+
///
473+
/// While payments will fail if most of `context` is modified, modifying
474+
/// [`OffersContext::InvoiceRequest::payment_metadata`] prior to blinded path construction is
475+
/// allowed.
476+
///
472477
fn create_blinded_paths<T: secp256k1::Signing + secp256k1::Verification>(
473478
&self, recipient: PublicKey, local_node_receive_key: ReceiveAuthKey,
474479
context: MessageContext, peers: Vec<MessageForwardNode>, secp_ctx: &Secp256k1<T>,

0 commit comments

Comments
 (0)