Skip to content

Commit f4b7ac7

Browse files
committed
Add a payment_metadata map in blinded payment path contexts
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 path contexts, offering a `BTreeMap<u64, Vec<u8>>` instead to enable more easily including multiple sets of data. Also note that a `Router` building a blinded path is allowed to modify the `payment_metadata` without breaking the payment. Tests by claude
1 parent 05135ae commit f4b7ac7

12 files changed

Lines changed: 348 additions & 32 deletions

File tree

fuzz/src/invoice_request_deser.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ fn build_response<T: secp256k1::Signing + secp256k1::Verification>(
104104
let payment_context = PaymentContext::Bolt12Offer(Bolt12OfferContext {
105105
offer_id: OfferId([42; 32]),
106106
invoice_request: invoice_request_fields,
107+
payment_metadata: None,
107108
});
108109
let payee_tlvs = ReceiveTlvs {
109110
payment_secret: PaymentSecret([42; 32]),

fuzz/src/refund_deser.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ fn build_response<T: secp256k1::Signing + secp256k1::Verification>(
6969
) -> Result<UnsignedBolt12Invoice, Bolt12SemanticError> {
7070
let entropy_source = Randomness {};
7171
let receive_auth_key = ReceiveAuthKey([41; 32]);
72-
let payment_context = PaymentContext::Bolt12Refund(Bolt12RefundContext {});
72+
let payment_context = PaymentContext::Bolt12Refund(Bolt12RefundContext { payment_metadata: None });
7373
let payee_tlvs = ReceiveTlvs {
7474
payment_secret: PaymentSecret([42; 32]),
7575
payment_constraints: PaymentConstraints {

lightning/src/blinded_path/payment.rs

Lines changed: 83 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99

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

@@ -29,8 +31,8 @@ use crate::types::features::BlindedHopFeatures;
2931
use crate::types::payment::PaymentSecret;
3032
use crate::types::routing::RoutingFees;
3133
use crate::util::ser::{
32-
FixedLengthReader, HighZeroBytesDroppedBigSize, LengthReadableArgs, Readable, WithoutLength,
33-
Writeable, Writer,
34+
BigSizeKeyedMap, FixedLengthReader, HighZeroBytesDroppedBigSize, LengthReadableArgs, Readable,
35+
WithoutLength, Writeable, Writer,
3436
};
3537

3638
#[allow(unused_imports)]
@@ -572,6 +574,16 @@ pub enum PaymentContext {
572574
/// [`Refund`]: crate::offers::refund::Refund
573575
Bolt12Refund(Bolt12RefundContext),
574576
}
577+
impl PaymentContext {
578+
/// Returns the additional payment metadata stored alongside this payment context, if any.
579+
pub fn payment_metadata(&self) -> Option<&BTreeMap<u64, Vec<u8>>> {
580+
match self {
581+
Self::Bolt12Offer(Bolt12OfferContext { payment_metadata, .. })
582+
| Self::AsyncBolt12Offer(AsyncBolt12OfferContext { payment_metadata, .. })
583+
| Self::Bolt12Refund(Bolt12RefundContext { payment_metadata, .. }) => payment_metadata.as_ref(),
584+
}
585+
}
586+
}
575587

576588
// Used when writing PaymentContext in Event::PaymentClaimable to avoid cloning.
577589
pub(crate) enum PaymentContextRef<'a> {
@@ -594,6 +606,22 @@ pub struct Bolt12OfferContext {
594606
/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
595607
/// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice
596608
pub invoice_request: InvoiceRequestFields,
609+
610+
/// Additional data about this payment which is not used in LDK and can be used for any
611+
/// purpose.
612+
///
613+
/// This is analogous to the BOLT 11 [`RecipientOnionFields::payment_metadata`] (which is
614+
/// provided to payers via [`Bolt11Invoice::payment_metadata`]) and can be used any time data
615+
/// needs to be "stored" by a payment recipient for their own internal use, provided back to
616+
/// them with the payment.
617+
///
618+
/// Note that because this is included in the payment onion, its size must be tightly
619+
/// constrained. More than a few hundred bytes and the payment will be entirely unpayable (with
620+
/// limited routing options as size increases).
621+
///
622+
/// [`RecipientOnionFields::payment_metadata`]: crate::ln::outbound_payment::RecipientOnionFields::payment_metadata
623+
/// [`Bolt11Invoice::payment_metadata`]: lightning_invoice::Bolt11Invoice::payment_metadata
624+
pub payment_metadata: Option<BTreeMap<u64, Vec<u8>>>,
597625
}
598626

599627
/// The context of a payment made for a static invoice requested from a BOLT 12 [`Offer`].
@@ -606,13 +634,45 @@ pub struct AsyncBolt12OfferContext {
606634
///
607635
/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
608636
pub offer_nonce: Nonce,
637+
638+
/// Additional data about this payment which is not used in LDK and can be used for any
639+
/// purpose.
640+
///
641+
/// This is analogous to the BOLT 11 [`RecipientOnionFields::payment_metadata`] (which is
642+
/// provided to payers via [`Bolt11Invoice::payment_metadata`]) and can be used any time data
643+
/// needs to be "stored" by a payment recipient for their own internal use, provided back to
644+
/// them with the payment.
645+
///
646+
/// Note that because this is included in the payment onion, its size must be tightly
647+
/// constrained. More than a few hundred bytes and the payment will be entirely unpayable (with
648+
/// limited routing options as size increases).
649+
///
650+
/// [`RecipientOnionFields::payment_metadata`]: crate::ln::outbound_payment::RecipientOnionFields::payment_metadata
651+
/// [`Bolt11Invoice::payment_metadata`]: lightning_invoice::Bolt11Invoice::payment_metadata
652+
pub payment_metadata: Option<BTreeMap<u64, Vec<u8>>>,
609653
}
610654

611655
/// The context of a payment made for an invoice sent for a BOLT 12 [`Refund`].
612656
///
613657
/// [`Refund`]: crate::offers::refund::Refund
614658
#[derive(Clone, Debug, Eq, PartialEq)]
615-
pub struct Bolt12RefundContext {}
659+
pub struct Bolt12RefundContext {
660+
/// Additional data about this payment which is not used in LDK and can be used for any
661+
/// purpose.
662+
///
663+
/// This is analogous to the BOLT 11 [`RecipientOnionFields::payment_metadata`] (which is
664+
/// provided to payers via [`Bolt11Invoice::payment_metadata`]) and can be used any time data
665+
/// needs to be "stored" by a payment recipient for their own internal use, provided back to
666+
/// them with the payment.
667+
///
668+
/// Note that because this is included in the payment onion, its size must be tightly
669+
/// constrained. More than a few hundred bytes and the payment will be entirely unpayable (with
670+
/// limited routing options as size increases).
671+
///
672+
/// [`RecipientOnionFields::payment_metadata`]: crate::ln::outbound_payment::RecipientOnionFields::payment_metadata
673+
/// [`Bolt11Invoice::payment_metadata`]: lightning_invoice::Bolt11Invoice::payment_metadata
674+
pub payment_metadata: Option<BTreeMap<u64, Vec<u8>>>,
675+
}
616676

617677
impl TryFrom<CounterpartyForwardingInfo> for PaymentRelay {
618678
type Error = ();
@@ -1031,14 +1091,18 @@ impl<'a> Writeable for PaymentContextRef<'a> {
10311091

10321092
impl_writeable_tlv_based!(Bolt12OfferContext, {
10331093
(0, offer_id, required),
1094+
(1, payment_metadata, (option, encoding: (BTreeMap<u64, Vec<u8>>, BigSizeKeyedMap))),
10341095
(2, invoice_request, required),
10351096
});
10361097

10371098
impl_writeable_tlv_based!(AsyncBolt12OfferContext, {
10381099
(0, offer_nonce, required),
1100+
(1, payment_metadata, (option, encoding: (BTreeMap<u64, Vec<u8>>, BigSizeKeyedMap))),
10391101
});
10401102

1041-
impl_writeable_tlv_based!(Bolt12RefundContext, {});
1103+
impl_writeable_tlv_based!(Bolt12RefundContext, {
1104+
(1, payment_metadata, (option, encoding: (BTreeMap<u64, Vec<u8>>, BigSizeKeyedMap))),
1105+
});
10421106

10431107
#[cfg(test)]
10441108
mod tests {
@@ -1097,7 +1161,9 @@ mod tests {
10971161
let recv_tlvs = ReceiveTlvs {
10981162
payment_secret: PaymentSecret([0; 32]),
10991163
payment_constraints: PaymentConstraints { max_cltv_expiry: 0, htlc_minimum_msat: 1 },
1100-
payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext {}),
1164+
payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext {
1165+
payment_metadata: None,
1166+
}),
11011167
};
11021168
let htlc_maximum_msat = 100_000;
11031169
let blinded_payinfo =
@@ -1115,7 +1181,9 @@ mod tests {
11151181
let recv_tlvs = ReceiveTlvs {
11161182
payment_secret: PaymentSecret([0; 32]),
11171183
payment_constraints: PaymentConstraints { max_cltv_expiry: 0, htlc_minimum_msat: 1 },
1118-
payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext {}),
1184+
payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext {
1185+
payment_metadata: None,
1186+
}),
11191187
};
11201188
let blinded_payinfo = super::compute_payinfo::<ForwardTlvs>(
11211189
&[],
@@ -1178,7 +1246,9 @@ mod tests {
11781246
let recv_tlvs = ReceiveTlvs {
11791247
payment_secret: PaymentSecret([0; 32]),
11801248
payment_constraints: PaymentConstraints { max_cltv_expiry: 0, htlc_minimum_msat: 3 },
1181-
payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext {}),
1249+
payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext {
1250+
payment_metadata: None,
1251+
}),
11821252
};
11831253
let htlc_maximum_msat = 100_000;
11841254
let blinded_payinfo = super::compute_payinfo(
@@ -1238,7 +1308,9 @@ mod tests {
12381308
let recv_tlvs = ReceiveTlvs {
12391309
payment_secret: PaymentSecret([0; 32]),
12401310
payment_constraints: PaymentConstraints { max_cltv_expiry: 0, htlc_minimum_msat: 1 },
1241-
payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext {}),
1311+
payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext {
1312+
payment_metadata: None,
1313+
}),
12421314
};
12431315
let htlc_minimum_msat = 3798;
12441316
assert!(super::compute_payinfo(
@@ -1309,7 +1381,9 @@ mod tests {
13091381
let recv_tlvs = ReceiveTlvs {
13101382
payment_secret: PaymentSecret([0; 32]),
13111383
payment_constraints: PaymentConstraints { max_cltv_expiry: 0, htlc_minimum_msat: 1 },
1312-
payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext {}),
1384+
payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext {
1385+
payment_metadata: None,
1386+
}),
13131387
};
13141388

13151389
let blinded_payinfo = super::compute_payinfo(

lightning/src/ln/async_payments_tests.rs

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
// You may not use this file except in accordance with one or both of these
88
// licenses.
99

10+
use alloc::collections::BTreeMap;
11+
1012
use crate::blinded_path::message::{
1113
BlindedMessagePath, MessageContext, NextMessageHop, OffersContext,
1214
};
@@ -299,6 +301,7 @@ fn create_static_invoice_builder<'a>(
299301
relative_expiry_secs,
300302
recipient.node.list_usable_channels(),
301303
recipient.node.test_get_peers_for_blinded_path(),
304+
None,
302305
)
303306
.unwrap()
304307
}
@@ -1150,6 +1153,88 @@ fn async_receive_flow_success() {
11501153
assert_eq!(res, Some(PaidBolt12Invoice::StaticInvoice(static_invoice)));
11511154
}
11521155

1156+
#[test]
1157+
fn async_payment_delivers_payment_metadata() {
1158+
// Test that `payment_metadata` set in the `AsyncBolt12OfferContext` of a static invoice's
1159+
// blinded payment paths is surfaced via `Event::PaymentClaimable` when the async recipient
1160+
// receives the keysend payment.
1161+
let chanmon_cfgs = create_chanmon_cfgs(3);
1162+
let node_cfgs = create_node_cfgs(3, &chanmon_cfgs);
1163+
1164+
let mut allow_priv_chan_fwds_cfg = test_default_channel_config();
1165+
allow_priv_chan_fwds_cfg.accept_forwards_to_priv_channels = true;
1166+
let node_chanmgrs =
1167+
create_node_chanmgrs(3, &node_cfgs, &[None, Some(allow_priv_chan_fwds_cfg), None]);
1168+
1169+
let nodes = create_network(3, &node_cfgs, &node_chanmgrs);
1170+
create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0);
1171+
create_unannounced_chan_between_nodes_with_value(&nodes, 1, 2, 1_000_000, 0);
1172+
1173+
let recipient_id = vec![42; 32];
1174+
let inv_server_paths =
1175+
nodes[1].node.blinded_paths_for_async_recipient(recipient_id.clone(), None).unwrap();
1176+
nodes[2].node.set_paths_to_static_invoice_server(inv_server_paths).unwrap();
1177+
expect_offer_paths_requests(&nodes[2], &[&nodes[0], &nodes[1]]);
1178+
1179+
// Configure the recipient's router to inject `payment_metadata` into the
1180+
// `AsyncBolt12OfferContext` of the static invoice's blinded payment paths. The
1181+
// `pass_static_invoice_server_messages` flow below builds the static invoice via this router,
1182+
// at which point the override is consumed.
1183+
let mut expected_metadata = BTreeMap::new();
1184+
expected_metadata.insert(0u64, vec![1, 2, 3, 4]);
1185+
expected_metadata.insert(7u64, vec![0xab, 0xcd]);
1186+
nodes[2].router.set_next_payment_context_metadata(expected_metadata.clone());
1187+
1188+
let invoice_flow_res =
1189+
pass_static_invoice_server_messages(&nodes[1], &nodes[2], recipient_id.clone());
1190+
let static_invoice = invoice_flow_res.invoice;
1191+
let offer = nodes[2].node.get_async_receive_offer().unwrap();
1192+
let amt_msat = 5000;
1193+
let payment_id = PaymentId([1; 32]);
1194+
nodes[0].node.pay_for_offer(&offer, Some(amt_msat), payment_id, Default::default()).unwrap();
1195+
let release_held_htlc_om = pass_async_payments_oms(
1196+
static_invoice.clone(),
1197+
&nodes[0],
1198+
&nodes[1],
1199+
&nodes[2],
1200+
recipient_id,
1201+
invoice_flow_res.invoice_request_path,
1202+
)
1203+
.1;
1204+
nodes[0]
1205+
.onion_messenger
1206+
.handle_onion_message(nodes[2].node.get_our_node_id(), &release_held_htlc_om);
1207+
1208+
let mut events = nodes[0].node.get_and_clear_pending_msg_events();
1209+
assert_eq!(events.len(), 1);
1210+
let ev = remove_first_msg_event_to_node(&nodes[1].node.get_our_node_id(), &mut events);
1211+
let payment_hash = extract_payment_hash(&ev);
1212+
check_added_monitors(&nodes[0], 1);
1213+
1214+
let route: &[&[&Node]] = &[&[&nodes[1], &nodes[2]]];
1215+
let args = PassAlongPathArgs::new(&nodes[0], route[0], amt_msat, payment_hash, ev)
1216+
.with_dummy_tlvs(&[DummyTlvs::default(); DEFAULT_PAYMENT_DUMMY_HOPS]);
1217+
let claimable_ev = do_pass_along_path(args).unwrap();
1218+
1219+
// Verify the `payment_metadata` we injected is surfaced via the `Bolt12OfferContext` of
1220+
// the `PaymentPurpose`. The recipient converts `AsyncBolt12OfferContext` to
1221+
// `Bolt12OfferContext` when constructing the `PaymentPurpose` for keysend payments.
1222+
match &claimable_ev {
1223+
Event::PaymentClaimable {
1224+
purpose: PaymentPurpose::Bolt12OfferPayment { payment_context, .. },
1225+
..
1226+
} => {
1227+
assert_eq!(payment_context.payment_metadata.as_ref(), Some(&expected_metadata));
1228+
},
1229+
_ => panic!("Unexpected event: {:?}", claimable_ev),
1230+
}
1231+
1232+
let keysend_preimage = extract_payment_preimage(&claimable_ev);
1233+
let (res, _) =
1234+
claim_payment_along_route(ClaimAlongRouteArgs::new(&nodes[0], route, keysend_preimage));
1235+
assert_eq!(res, Some(PaidBolt12Invoice::StaticInvoice(static_invoice)));
1236+
}
1237+
11531238
#[cfg_attr(feature = "std", ignore)]
11541239
#[test]
11551240
fn expired_static_invoice_fail() {
@@ -1591,6 +1676,7 @@ fn reject_bad_payment_secret() {
15911676
PaymentContext::AsyncBolt12Offer(AsyncBolt12OfferContext {
15921677
// We don't reach the point of checking the invreq nonce due to the invalid payment secret
15931678
offer_nonce: Nonce([i; Nonce::LENGTH]),
1679+
payment_metadata: None,
15941680
}),
15951681
u32::MAX,
15961682
)
@@ -3123,7 +3209,10 @@ fn intercepted_hold_htlc() {
31233209
.unwrap();
31243210
let mut offer_nonce = Nonce([0; Nonce::LENGTH]);
31253211
offer_nonce.0.copy_from_slice(&hardcoded_random_bytes[..Nonce::LENGTH]);
3126-
let payment_context = PaymentContext::AsyncBolt12Offer(AsyncBolt12OfferContext { offer_nonce });
3212+
let payment_context = PaymentContext::AsyncBolt12Offer(AsyncBolt12OfferContext {
3213+
offer_nonce,
3214+
payment_metadata: None,
3215+
});
31273216
let blinded_payment_path_with_jit_channel_scid = recipient
31283217
.node
31293218
.flow

0 commit comments

Comments
 (0)