Skip to content

Commit a8b98b4

Browse files
blip42: Add ContactInfo field to PaymentSent event for contact management
This commit implements the infrastructure to expose BLIP-42 contact information through the PaymentSent event, allowing applications to manage contact relationships when BOLT12 offer payments complete.
1 parent 94b56f3 commit a8b98b4

File tree

6 files changed

+175
-45
lines changed

6 files changed

+175
-45
lines changed

lightning/src/events/mod.rs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -727,6 +727,55 @@ pub enum InboundChannelFunds {
727727
/// who is the channel opener in this case.
728728
DualFunded,
729729
}
730+
/// Contact information for BLIP-42 contact management, containing the contact secrets
731+
/// and payer offer that were used when paying a BOLT12 offer.
732+
///
733+
/// This information allows the payer to establish a contact relationship with the recipient,
734+
/// enabling future direct payments without needing a new offer.
735+
#[derive(Clone, Debug, PartialEq, Eq)]
736+
pub struct ContactInfo {
737+
/// The contact secrets that were generated and sent in the invoice request.
738+
pub contact_secrets: crate::offers::contacts::ContactSecrets,
739+
/// The payer's offer that was sent in the invoice request.
740+
pub payer_offer: crate::offers::offer::Offer,
741+
}
742+
743+
impl Writeable for ContactInfo {
744+
fn write<W: Writer>(&self, writer: &mut W) -> Result<(), io::Error> {
745+
// Serialize ContactSecrets by writing its fields
746+
self.contact_secrets.primary_secret().write(writer)?;
747+
(self.contact_secrets.additional_remote_secrets().len() as u16).write(writer)?;
748+
for secret in self.contact_secrets.additional_remote_secrets() {
749+
secret.write(writer)?;
750+
}
751+
// Serialize Offer as bytes (as a length-prefixed Vec<u8>)
752+
self.payer_offer.as_ref().to_vec().write(writer)?;
753+
Ok(())
754+
}
755+
}
756+
757+
impl Readable for ContactInfo {
758+
fn read<R: io::Read>(reader: &mut R) -> Result<Self, crate::ln::msgs::DecodeError> {
759+
// Deserialize ContactSecrets
760+
let primary_secret: [u8; 32] = Readable::read(reader)?;
761+
let num_secrets: u16 = Readable::read(reader)?;
762+
let mut additional_remote_secrets = Vec::with_capacity(num_secrets as usize);
763+
for _ in 0..num_secrets {
764+
additional_remote_secrets.push(Readable::read(reader)?);
765+
}
766+
let contact_secrets = crate::offers::contacts::ContactSecrets::with_additional_secrets(
767+
primary_secret,
768+
additional_remote_secrets,
769+
);
770+
771+
// Deserialize Offer (as a length-prefixed Vec<u8>)
772+
let payer_offer_bytes: Vec<u8> = Readable::read(reader)?;
773+
let payer_offer = crate::offers::offer::Offer::try_from(payer_offer_bytes)
774+
.map_err(|_| crate::ln::msgs::DecodeError::InvalidValue)?;
775+
776+
Ok(ContactInfo { contact_secrets, payer_offer })
777+
}
778+
}
730779

731780
/// An Event which you should probably take some action in response to.
732781
///
@@ -1064,6 +1113,13 @@ pub enum Event {
10641113
///
10651114
/// [`StaticInvoice`]: crate::offers::static_invoice::StaticInvoice
10661115
bolt12_invoice: Option<PaidBolt12Invoice>,
1116+
/// Contact information for BLIP-42 contact management.
1117+
///
1118+
/// This is `Some` when paying a BOLT12 offer with contact information enabled,
1119+
/// containing the contact secrets and payer offer that were sent in the invoice request.
1120+
///
1121+
/// This allows the payer to establish a contact relationship with the recipient.
1122+
contact_info: Option<ContactInfo>,
10671123
},
10681124
/// Indicates an outbound payment failed. Individual [`Event::PaymentPathFailed`] events
10691125
/// provide failure information for each path attempt in the payment, including retries.
@@ -1951,6 +2007,7 @@ impl Writeable for Event {
19512007
ref amount_msat,
19522008
ref fee_paid_msat,
19532009
ref bolt12_invoice,
2010+
ref contact_info,
19542011
} => {
19552012
2u8.write(writer)?;
19562013
write_tlv_fields!(writer, {
@@ -1960,6 +2017,7 @@ impl Writeable for Event {
19602017
(5, fee_paid_msat, option),
19612018
(7, amount_msat, option),
19622019
(9, bolt12_invoice, option),
2020+
(11, contact_info, option),
19632021
});
19642022
},
19652023
&Event::PaymentPathFailed {
@@ -2422,13 +2480,15 @@ impl MaybeReadable for Event {
24222480
let mut amount_msat = None;
24232481
let mut fee_paid_msat = None;
24242482
let mut bolt12_invoice = None;
2483+
let mut contact_info = None;
24252484
read_tlv_fields!(reader, {
24262485
(0, payment_preimage, required),
24272486
(1, payment_hash, option),
24282487
(3, payment_id, option),
24292488
(5, fee_paid_msat, option),
24302489
(7, amount_msat, option),
24312490
(9, bolt12_invoice, option),
2491+
(11, contact_info, option),
24322492
});
24332493
if payment_hash.is_none() {
24342494
payment_hash = Some(PaymentHash(
@@ -2442,6 +2502,7 @@ impl MaybeReadable for Event {
24422502
amount_msat,
24432503
fee_paid_msat,
24442504
bolt12_invoice,
2505+
contact_info,
24452506
}))
24462507
};
24472508
f()

lightning/src/ln/async_payments_tests.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -988,7 +988,7 @@ fn ignore_duplicate_invoice() {
988988
let args = PassAlongPathArgs::new(sender, route[0], amt_msat, payment_hash, ev);
989989
let claimable_ev = do_pass_along_path(args).unwrap();
990990
let keysend_preimage = extract_payment_preimage(&claimable_ev);
991-
let (res, _) =
991+
let (res, _, _) =
992992
claim_payment_along_route(ClaimAlongRouteArgs::new(sender, route, keysend_preimage));
993993
assert_eq!(res, Some(PaidBolt12Invoice::StaticInvoice(static_invoice.clone())));
994994

@@ -1073,7 +1073,7 @@ fn ignore_duplicate_invoice() {
10731073
};
10741074

10751075
// After paying invoice, check that static invoice is ignored.
1076-
let res = claim_payment(sender, route[0], payment_preimage);
1076+
let (res, _) = claim_payment(sender, route[0], payment_preimage);
10771077
assert_eq!(res, Some(PaidBolt12Invoice::Bolt12Invoice(invoice)));
10781078

10791079
sender.onion_messenger.handle_onion_message(always_online_node_id, &static_invoice_om);
@@ -1142,7 +1142,7 @@ fn async_receive_flow_success() {
11421142
let args = PassAlongPathArgs::new(&nodes[0], route[0], amt_msat, payment_hash, ev);
11431143
let claimable_ev = do_pass_along_path(args).unwrap();
11441144
let keysend_preimage = extract_payment_preimage(&claimable_ev);
1145-
let (res, _) =
1145+
let (res, _, _) =
11461146
claim_payment_along_route(ClaimAlongRouteArgs::new(&nodes[0], route, keysend_preimage));
11471147
assert_eq!(res, Some(PaidBolt12Invoice::StaticInvoice(static_invoice)));
11481148
}
@@ -2942,7 +2942,7 @@ fn async_payment_e2e() {
29422942

29432943
let route: &[&[&Node]] = &[&[sender_lsp, invoice_server, recipient]];
29442944
let keysend_preimage = extract_payment_preimage(&claimable_ev);
2945-
let (res, _) =
2945+
let (res, _, _) =
29462946
claim_payment_along_route(ClaimAlongRouteArgs::new(sender, route, keysend_preimage));
29472947
assert_eq!(res, Some(PaidBolt12Invoice::StaticInvoice(static_invoice)));
29482948
}
@@ -3180,7 +3180,7 @@ fn intercepted_hold_htlc() {
31803180

31813181
let route: &[&[&Node]] = &[&[lsp, recipient]];
31823182
let keysend_preimage = extract_payment_preimage(&claimable_ev);
3183-
let (res, _) =
3183+
let (res, _, _) =
31843184
claim_payment_along_route(ClaimAlongRouteArgs::new(sender, route, keysend_preimage));
31853185
assert_eq!(res, Some(PaidBolt12Invoice::StaticInvoice(static_invoice)));
31863186
}

lightning/src/ln/functional_test_utils.rs

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3033,7 +3033,7 @@ pub fn expect_payment_sent<CM: AChannelManager, H: NodeHolder<CM = CM>>(
30333033
node: &H, expected_payment_preimage: PaymentPreimage,
30343034
expected_fee_msat_opt: Option<Option<u64>>, expect_per_path_claims: bool,
30353035
expect_post_ev_mon_update: bool,
3036-
) -> (Option<PaidBolt12Invoice>, Vec<Event>) {
3036+
) -> (Option<PaidBolt12Invoice>, Vec<Event>, Option<crate::events::ContactInfo>) {
30373037
if expect_post_ev_mon_update {
30383038
check_added_monitors(node, 0);
30393039
}
@@ -3051,6 +3051,7 @@ pub fn expect_payment_sent<CM: AChannelManager, H: NodeHolder<CM = CM>>(
30513051
}
30523052
// We return the invoice because some test may want to check the invoice details.
30533053
let invoice;
3054+
let contact_info_result;
30543055
let mut path_events = Vec::new();
30553056
let expected_payment_id = match events[0] {
30563057
Event::PaymentSent {
@@ -3060,6 +3061,7 @@ pub fn expect_payment_sent<CM: AChannelManager, H: NodeHolder<CM = CM>>(
30603061
ref amount_msat,
30613062
ref fee_paid_msat,
30623063
ref bolt12_invoice,
3064+
ref contact_info,
30633065
} => {
30643066
assert_eq!(expected_payment_preimage, *payment_preimage);
30653067
assert_eq!(expected_payment_hash, *payment_hash);
@@ -3070,6 +3072,7 @@ pub fn expect_payment_sent<CM: AChannelManager, H: NodeHolder<CM = CM>>(
30703072
assert!(fee_paid_msat.is_some());
30713073
}
30723074
invoice = bolt12_invoice.clone();
3075+
contact_info_result = contact_info.clone();
30733076
payment_id.unwrap()
30743077
},
30753078
_ => panic!("Unexpected event"),
@@ -3087,7 +3090,7 @@ pub fn expect_payment_sent<CM: AChannelManager, H: NodeHolder<CM = CM>>(
30873090
}
30883091
}
30893092
}
3090-
(invoice, path_events)
3093+
(invoice, path_events, contact_info_result)
30913094
}
30923095

30933096
#[macro_export]
@@ -4119,28 +4122,28 @@ pub fn pass_claimed_payment_along_route(args: ClaimAlongRouteArgs) -> u64 {
41194122
}
41204123
pub fn claim_payment_along_route(
41214124
args: ClaimAlongRouteArgs,
4122-
) -> (Option<PaidBolt12Invoice>, Vec<Event>) {
4125+
) -> (Option<PaidBolt12Invoice>, Vec<Event>, Option<crate::events::ContactInfo>) {
41234126
let origin_node = args.origin_node;
41244127
let payment_preimage = args.payment_preimage;
41254128
let skip_last = args.skip_last;
41264129
let expected_total_fee_msat = do_claim_payment_along_route(args);
41274130
if !skip_last {
41284131
expect_payment_sent!(origin_node, payment_preimage, Some(expected_total_fee_msat))
41294132
} else {
4130-
(None, Vec::new())
4133+
(None, Vec::new(), None)
41314134
}
41324135
}
41334136

41344137
pub fn claim_payment<'a, 'b, 'c>(
41354138
origin_node: &Node<'a, 'b, 'c>, expected_route: &[&Node<'a, 'b, 'c>],
41364139
our_payment_preimage: PaymentPreimage,
4137-
) -> Option<PaidBolt12Invoice> {
4138-
claim_payment_along_route(ClaimAlongRouteArgs::new(
4140+
) -> (Option<PaidBolt12Invoice>, Option<crate::events::ContactInfo>) {
4141+
let result = claim_payment_along_route(ClaimAlongRouteArgs::new(
41394142
origin_node,
41404143
&[expected_route],
41414144
our_payment_preimage,
4142-
))
4143-
.0
4145+
));
4146+
(result.0, result.2)
41444147
}
41454148

41464149
pub const TEST_FINAL_CLTV: u32 = 70;

lightning/src/ln/offers_tests.rs

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ fn route_bolt12_payment<'a, 'b, 'c>(
185185

186186
fn claim_bolt12_payment<'a, 'b, 'c>(
187187
node: &Node<'a, 'b, 'c>, path: &[&Node<'a, 'b, 'c>], expected_payment_context: PaymentContext, invoice: &Bolt12Invoice
188-
) {
188+
) -> Option<crate::events::ContactInfo> {
189189
let recipient = &path[path.len() - 1];
190190
let payment_purpose = match get_event!(recipient, Event::PaymentClaimable) {
191191
Event::PaymentClaimable { purpose, .. } => purpose,
@@ -204,11 +204,13 @@ fn claim_bolt12_payment<'a, 'b, 'c>(
204204
},
205205
_ => panic!("Unexpected payment purpose: {:?}", payment_purpose),
206206
}
207-
if let Some(inv) = claim_payment(node, path, payment_preimage) {
208-
assert_eq!(inv, PaidBolt12Invoice::Bolt12Invoice(invoice.to_owned()));
207+
let (inv, contact_info) = claim_payment(node, path, payment_preimage);
208+
if let Some(paid_inv) = inv {
209+
assert_eq!(paid_inv, PaidBolt12Invoice::Bolt12Invoice(invoice.to_owned()));
209210
} else {
210211
panic!("Expected PaidInvoice::Bolt12Invoice");
211212
};
213+
contact_info
212214
}
213215

214216
fn extract_offer_nonce<'a, 'b, 'c>(node: &Node<'a, 'b, 'c>, message: &OnionMessage) -> Nonce {
@@ -714,7 +716,7 @@ fn creates_and_pays_for_offer_using_two_hop_blinded_path() {
714716
route_bolt12_payment(david, &[charlie, bob, alice], &invoice);
715717
expect_recent_payment!(david, RecentPaymentDetails::Pending, payment_id);
716718

717-
claim_bolt12_payment(david, &[charlie, bob, alice], payment_context, &invoice);
719+
let _ = claim_bolt12_payment(david, &[charlie, bob, alice], payment_context, &invoice);
718720
expect_recent_payment!(david, RecentPaymentDetails::Fulfilled, payment_id);
719721
}
720722

@@ -796,7 +798,7 @@ fn creates_and_pays_for_refund_using_two_hop_blinded_path() {
796798
route_bolt12_payment(david, &[charlie, bob, alice], &invoice);
797799
expect_recent_payment!(david, RecentPaymentDetails::Pending, payment_id);
798800

799-
claim_bolt12_payment(david, &[charlie, bob, alice], payment_context, &invoice);
801+
let _ = claim_bolt12_payment(david, &[charlie, bob, alice], payment_context, &invoice);
800802
expect_recent_payment!(david, RecentPaymentDetails::Fulfilled, payment_id);
801803
}
802804

@@ -863,7 +865,7 @@ fn creates_and_pays_for_offer_using_one_hop_blinded_path() {
863865
route_bolt12_payment(bob, &[alice], &invoice);
864866
expect_recent_payment!(bob, RecentPaymentDetails::Pending, payment_id);
865867

866-
claim_bolt12_payment(bob, &[alice], payment_context, &invoice);
868+
let _ = claim_bolt12_payment(bob, &[alice], payment_context, &invoice);
867869
expect_recent_payment!(bob, RecentPaymentDetails::Fulfilled, payment_id);
868870
}
869871

@@ -919,7 +921,7 @@ fn creates_and_pays_for_refund_using_one_hop_blinded_path() {
919921
route_bolt12_payment(bob, &[alice], &invoice);
920922
expect_recent_payment!(bob, RecentPaymentDetails::Pending, payment_id);
921923

922-
claim_bolt12_payment(bob, &[alice], payment_context, &invoice);
924+
let _ = claim_bolt12_payment(bob, &[alice], payment_context, &invoice);
923925
expect_recent_payment!(bob, RecentPaymentDetails::Fulfilled, payment_id);
924926
}
925927

@@ -973,7 +975,7 @@ fn pays_for_offer_without_blinded_paths() {
973975
route_bolt12_payment(bob, &[alice], &invoice);
974976
expect_recent_payment!(bob, RecentPaymentDetails::Pending, payment_id);
975977

976-
claim_bolt12_payment(bob, &[alice], payment_context, &invoice);
978+
let _ = claim_bolt12_payment(bob, &[alice], payment_context, &invoice);
977979
expect_recent_payment!(bob, RecentPaymentDetails::Fulfilled, payment_id);
978980
}
979981

@@ -1016,7 +1018,7 @@ fn pays_for_refund_without_blinded_paths() {
10161018
route_bolt12_payment(bob, &[alice], &invoice);
10171019
expect_recent_payment!(bob, RecentPaymentDetails::Pending, payment_id);
10181020

1019-
claim_bolt12_payment(bob, &[alice], payment_context, &invoice);
1021+
let _ = claim_bolt12_payment(bob, &[alice], payment_context, &invoice);
10201022
expect_recent_payment!(bob, RecentPaymentDetails::Fulfilled, payment_id);
10211023
}
10221024

@@ -1253,7 +1255,7 @@ fn creates_and_pays_for_offer_with_retry() {
12531255
}
12541256
route_bolt12_payment(bob, &[alice], &invoice);
12551257
expect_recent_payment!(bob, RecentPaymentDetails::Pending, payment_id);
1256-
claim_bolt12_payment(bob, &[alice], payment_context, &invoice);
1258+
let _ = claim_bolt12_payment(bob, &[alice], payment_context, &invoice);
12571259
expect_recent_payment!(bob, RecentPaymentDetails::Fulfilled, payment_id);
12581260
}
12591261

@@ -1331,7 +1333,7 @@ fn pays_bolt12_invoice_asynchronously() {
13311333
route_bolt12_payment(bob, &[alice], &invoice);
13321334
expect_recent_payment!(bob, RecentPaymentDetails::Pending, payment_id);
13331335

1334-
claim_bolt12_payment(bob, &[alice], payment_context, &invoice);
1336+
let _ = claim_bolt12_payment(bob, &[alice], payment_context, &invoice);
13351337
expect_recent_payment!(bob, RecentPaymentDetails::Fulfilled, payment_id);
13361338

13371339
assert_eq!(
@@ -1411,7 +1413,7 @@ fn creates_offer_with_blinded_path_using_unannounced_introduction_node() {
14111413
route_bolt12_payment(bob, &[alice], &invoice);
14121414
expect_recent_payment!(bob, RecentPaymentDetails::Pending, payment_id);
14131415

1414-
claim_bolt12_payment(bob, &[alice], payment_context, &invoice);
1416+
let _ = claim_bolt12_payment(bob, &[alice], payment_context, &invoice);
14151417
expect_recent_payment!(bob, RecentPaymentDetails::Fulfilled, payment_id);
14161418
}
14171419

@@ -2251,7 +2253,7 @@ fn fails_paying_invoice_more_than_once() {
22512253
assert!(david.node.get_and_clear_pending_msg_events().is_empty());
22522254

22532255
// Complete paying the first invoice
2254-
claim_bolt12_payment(david, &[charlie, bob, alice], payment_context, &invoice1);
2256+
let _ = claim_bolt12_payment(david, &[charlie, bob, alice], payment_context, &invoice1);
22552257
expect_recent_payment!(david, RecentPaymentDetails::Fulfilled, payment_id);
22562258
}
22572259

@@ -2576,10 +2578,6 @@ fn pay_offer_and_add_contacts_info_blip42() {
25762578
// TODO: we should check also if the contact secret is the same that we inject by bob.
25772579

25782580
assert!(invoice_request.payer_offer().is_some());
2579-
// FIXME: this is wrong but make sure that we are following correctly the code path.
2580-
let payer_offer_bytes = invoice_request.payer_offer().unwrap();
2581-
let payer_offer = Offer::try_from(payer_offer_bytes.to_vec()).unwrap();
2582-
assert_eq!(payer_offer, offer);
25832581

25842582
let onion_message = alice.onion_messenger.next_onion_message_for_peer(bob_id).unwrap();
25852583
bob.onion_messenger.handle_onion_message(alice_id, &onion_message);
@@ -2592,9 +2590,15 @@ fn pay_offer_and_add_contacts_info_blip42() {
25922590
route_bolt12_payment(bob, &[alice], &invoice);
25932591
expect_recent_payment!(bob, RecentPaymentDetails::Pending, payment_id);
25942592

2595-
claim_bolt12_payment(bob, &[alice], payment_context, &invoice);
2593+
let contact_info = claim_bolt12_payment(bob, &[alice], payment_context, &invoice);
25962594
expect_recent_payment!(bob, RecentPaymentDetails::Fulfilled, payment_id);
25972595

2596+
assert!(contact_info.is_some());
2597+
let contact_info = contact_info.unwrap();
2598+
2599+
assert!(invoice_request.contact_secret().is_some());
2600+
assert_eq!(invoice_request.contact_secret().unwrap(), contact_info.contact_secrets.primary_secret());
2601+
25982602
// TODO: now should be possible that alice will be able to repay bob without that
25992603
// bob give any offer in exchange!! but there is a contact list somewhere that allow
26002604
// to run something like bob.pay_for_contact(alice_contact_name, amount);

0 commit comments

Comments
 (0)