Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
7f2d806
Add experimental TLV fields for invoice requests: invreq_contact_secr…
vincenzopalazzo Sep 24, 2025
2d1a9a6
Add contacts module for Lightning offer contact management
vincenzopalazzo Sep 30, 2025
a54d2ba
blip42: mock the test workflow
vincenzopalazzo Oct 1, 2025
ec57c10
blip42: add the possibility to inject conctact secret inside the pay …
vincenzopalazzo Oct 3, 2025
4cf24a6
Add payer_offer() accessor method for invoice requests
vincenzopalazzo Nov 3, 2025
cbd75f2
Inject payer offer into invoice requests for BLIP-42
vincenzopalazzo Nov 3, 2025
307c9da
blip42: Add ContactInfo field to PaymentSent event for contact manage…
vincenzopalazzo Nov 3, 2025
ad3dd27
receiver side exstract the payer_offer from a PaymentClaimable
vincenzopalazzo Nov 5, 2025
88e9a52
fix the rebase stuff
vincenzopalazzo Nov 6, 2025
d50d1b1
fixup! blip42: add the possibility to inject conctact secret inside t…
vincenzopalazzo Nov 18, 2025
62808ec
fixup! Add experimental TLV fields for invoice requests: invreq_conta…
vincenzopalazzo Nov 18, 2025
ba7e9e9
fixup! blip42: Add ContactInfo field to PaymentSent event for contact…
vincenzopalazzo Nov 18, 2025
0a43a30
fixup! Add payer_offer() accessor method for invoice requests
vincenzopalazzo Nov 18, 2025
755fed4
fixup! blip42: Add ContactInfo field to PaymentSent event for contact…
vincenzopalazzo Nov 18, 2025
b35a065
fixup! Add contacts module for Lightning offer contact management
vincenzopalazzo Nov 18, 2025
d001559
fixup! blip42: Add ContactInfo field to PaymentSent event for contact…
vincenzopalazzo Dec 1, 2025
e147725
fixup! blip42: add the possibility to inject conctact secret inside t…
vincenzopalazzo Dec 1, 2025
8a24949
f use a smaller offer
vincenzopalazzo Dec 1, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions lightning/src/events/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -727,6 +727,55 @@ pub enum InboundChannelFunds {
/// who is the channel opener in this case.
DualFunded,
}
/// Contact information for BLIP-42 contact management, containing the contact secrets
/// and payer offer that were used when paying a BOLT12 offer.
///
/// This information allows the payer to establish a contact relationship with the recipient,
/// enabling future direct payments without needing a new offer.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ContactInfo {
/// The contact secrets that were generated and sent in the invoice request.
pub contact_secrets: crate::offers::contacts::ContactSecrets,
/// The payer's offer that was sent in the invoice request.
pub payer_offer: crate::offers::offer::Offer,
}

impl Writeable for ContactInfo {
fn write<W: Writer>(&self, writer: &mut W) -> Result<(), io::Error> {
// Serialize ContactSecrets by writing its fields
self.contact_secrets.primary_secret().write(writer)?;
(self.contact_secrets.additional_remote_secrets().len() as u16).write(writer)?;
for secret in self.contact_secrets.additional_remote_secrets() {
secret.write(writer)?;
}
// Serialize Offer as bytes (as a length-prefixed Vec<u8>)
self.payer_offer.as_ref().to_vec().write(writer)?;
Ok(())
}
}

impl Readable for ContactInfo {
fn read<R: io::Read>(reader: &mut R) -> Result<Self, crate::ln::msgs::DecodeError> {
// Deserialize ContactSecrets
let primary_secret: [u8; 32] = Readable::read(reader)?;
let num_secrets: u16 = Readable::read(reader)?;
let mut additional_remote_secrets = Vec::with_capacity(num_secrets as usize);
for _ in 0..num_secrets {
additional_remote_secrets.push(Readable::read(reader)?);
}
let contact_secrets = crate::offers::contacts::ContactSecrets::with_additional_secrets(
primary_secret,
additional_remote_secrets,
);

// Deserialize Offer (as a length-prefixed Vec<u8>)
let payer_offer_bytes: Vec<u8> = Readable::read(reader)?;
let payer_offer = crate::offers::offer::Offer::try_from(payer_offer_bytes)
.map_err(|_| crate::ln::msgs::DecodeError::InvalidValue)?;

Ok(ContactInfo { contact_secrets, payer_offer })
}
}

/// An Event which you should probably take some action in response to.
///
Expand Down Expand Up @@ -1064,6 +1113,13 @@ pub enum Event {
///
/// [`StaticInvoice`]: crate::offers::static_invoice::StaticInvoice
bolt12_invoice: Option<PaidBolt12Invoice>,
/// Contact information for BLIP-42 contact management.
///
/// This is `Some` when paying a BOLT12 offer with contact information enabled,
/// containing the contact secrets and payer offer that were sent in the invoice request.
///
/// This allows the payer to establish a contact relationship with the recipient.
contact_info: Option<ContactInfo>,
},
/// Indicates an outbound payment failed. Individual [`Event::PaymentPathFailed`] events
/// provide failure information for each path attempt in the payment, including retries.
Expand Down Expand Up @@ -1951,6 +2007,7 @@ impl Writeable for Event {
ref amount_msat,
ref fee_paid_msat,
ref bolt12_invoice,
ref contact_info,
} => {
2u8.write(writer)?;
write_tlv_fields!(writer, {
Expand All @@ -1960,6 +2017,7 @@ impl Writeable for Event {
(5, fee_paid_msat, option),
(7, amount_msat, option),
(9, bolt12_invoice, option),
(11, contact_info, option),
});
},
&Event::PaymentPathFailed {
Expand Down Expand Up @@ -2422,13 +2480,15 @@ impl MaybeReadable for Event {
let mut amount_msat = None;
let mut fee_paid_msat = None;
let mut bolt12_invoice = None;
let mut contact_info = None;
read_tlv_fields!(reader, {
(0, payment_preimage, required),
(1, payment_hash, option),
(3, payment_id, option),
(5, fee_paid_msat, option),
(7, amount_msat, option),
(9, bolt12_invoice, option),
(11, contact_info, option),
});
if payment_hash.is_none() {
payment_hash = Some(PaymentHash(
Expand All @@ -2442,6 +2502,7 @@ impl MaybeReadable for Event {
amount_msat,
fee_paid_msat,
bolt12_invoice,
contact_info,
}))
};
f()
Expand Down
12 changes: 6 additions & 6 deletions lightning/src/ln/async_payments_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -988,7 +988,7 @@ fn ignore_duplicate_invoice() {
let args = PassAlongPathArgs::new(sender, route[0], amt_msat, payment_hash, ev);
let claimable_ev = do_pass_along_path(args).unwrap();
let keysend_preimage = extract_payment_preimage(&claimable_ev);
let (res, _) =
let (res, _, _) =
claim_payment_along_route(ClaimAlongRouteArgs::new(sender, route, keysend_preimage));
assert_eq!(res, Some(PaidBolt12Invoice::StaticInvoice(static_invoice.clone())));

Expand Down Expand Up @@ -1073,7 +1073,7 @@ fn ignore_duplicate_invoice() {
};

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

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

let route: &[&[&Node]] = &[&[sender_lsp, invoice_server, recipient]];
let keysend_preimage = extract_payment_preimage(&claimable_ev);
let (res, _) =
let (res, _, _) =
claim_payment_along_route(ClaimAlongRouteArgs::new(sender, route, keysend_preimage));
assert_eq!(res, Some(PaidBolt12Invoice::StaticInvoice(static_invoice)));
}
Expand Down Expand Up @@ -3180,7 +3180,7 @@ fn intercepted_hold_htlc() {

let route: &[&[&Node]] = &[&[lsp, recipient]];
let keysend_preimage = extract_payment_preimage(&claimable_ev);
let (res, _) =
let (res, _, _) =
claim_payment_along_route(ClaimAlongRouteArgs::new(sender, route, keysend_preimage));
assert_eq!(res, Some(PaidBolt12Invoice::StaticInvoice(static_invoice)));
}
Expand Down Expand Up @@ -3427,7 +3427,7 @@ fn release_htlc_races_htlc_onion_decode() {

let route: &[&[&Node]] = &[&[sender_lsp, invoice_server, recipient]];
let keysend_preimage = extract_payment_preimage(&claimable_ev);
let (res, _) =
let (res, _, _) =
claim_payment_along_route(ClaimAlongRouteArgs::new(sender, route, keysend_preimage));
assert_eq!(res, Some(PaidBolt12Invoice::StaticInvoice(static_invoice)));
}
22 changes: 21 additions & 1 deletion lightning/src/ln/channelmanager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ use crate::ln::outbound_payment::{
};
use crate::ln::types::ChannelId;
use crate::offers::async_receive_offer_cache::AsyncReceiveOfferCache;
use crate::offers::contacts::ContactSecrets;
use crate::offers::flow::{HeldHtlcReplyPath, InvreqResponseInstructions, OffersMessageFlow};
use crate::offers::invoice::{Bolt12Invoice, UnsignedBolt12Invoice};
use crate::offers::invoice_error::InvoiceError;
Expand Down Expand Up @@ -728,6 +729,9 @@ pub struct OptionalOfferPaymentParams {
/// will ultimately fail once all pending paths have failed (generating an
/// [`Event::PaymentFailed`]).
pub retry_strategy: Retry,
/// Contact secrets to include in the invoice request for BLIP-42 contact management.
/// If provided, these secrets will be used to establish a contact relationship with the recipient.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to be substantially more filled-out, including information about intended UX and UI integration logic, how/when to derive secrets, etc.

pub contact_secrects: Option<ContactSecrets>,
}

impl Default for OptionalOfferPaymentParams {
Expand All @@ -739,6 +743,7 @@ impl Default for OptionalOfferPaymentParams {
retry_strategy: Retry::Timeout(core::time::Duration::from_secs(2)),
#[cfg(not(feature = "std"))]
retry_strategy: Retry::Attempts(3),
contact_secrects: None,
}
}
}
Expand Down Expand Up @@ -12944,6 +12949,7 @@ where
payment_id,
None,
create_pending_payment_fn,
optional_params.contact_secrects,
)
}

Expand Down Expand Up @@ -12973,6 +12979,7 @@ where
payment_id,
Some(offer.hrn),
create_pending_payment_fn,
optional_params.contact_secrects,
)
}

Expand Down Expand Up @@ -13015,6 +13022,7 @@ where
payment_id,
None,
create_pending_payment_fn,
optional_params.contact_secrects,
)
}

Expand All @@ -13023,6 +13031,7 @@ where
&self, offer: &Offer, quantity: Option<u64>, amount_msats: Option<u64>,
payer_note: Option<String>, payment_id: PaymentId,
human_readable_name: Option<HumanReadableName>, create_pending_payment: CPP,
contacts: Option<ContactSecrets>,
) -> Result<(), Bolt12SemanticError> {
let entropy = &*self.entropy_source;
let nonce = Nonce::from_entropy_source(entropy);
Expand All @@ -13048,6 +13057,17 @@ where
Some(hrn) => builder.sourced_from_human_readable_name(hrn),
};

let builder = if let Some(secrets) = contacts.as_ref() {
builder.contact_secrets(secrets.clone())
} else {
builder
};
// Create a minimal compact offer for BLIP-42 contact exchange.
// This uses derived metadata (for verification) but no blinded paths, making it small
// enough to fit in the onion packet (~70 bytes vs 300+ bytes with blinded paths).
let payer_offer = self.flow.create_compact_offer_builder(&*self.entropy_source)?.build()?;
let builder = builder.payer_offer(&payer_offer);

let invoice_request = builder.build_and_sign()?;
let _persistence_guard = PersistenceNotifierGuard::notify_on_drop(self);

Expand Down Expand Up @@ -15649,7 +15669,7 @@ where
self.pending_outbound_payments
.received_offer(payment_id, Some(retryable_invoice_request))
.map_err(|_| Bolt12SemanticError::DuplicatePaymentId)
});
}, None);
if offer_pay_res.is_err() {
// The offer we tried to pay is the canonical current offer for the name we
// wanted to pay. If we can't pay it, there's no way to recover so fail the
Expand Down
19 changes: 11 additions & 8 deletions lightning/src/ln/functional_test_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3033,7 +3033,7 @@ pub fn expect_payment_sent<CM: AChannelManager, H: NodeHolder<CM = CM>>(
node: &H, expected_payment_preimage: PaymentPreimage,
expected_fee_msat_opt: Option<Option<u64>>, expect_per_path_claims: bool,
expect_post_ev_mon_update: bool,
) -> (Option<PaidBolt12Invoice>, Vec<Event>) {
) -> (Option<PaidBolt12Invoice>, Vec<Event>, Option<crate::events::ContactInfo>) {
if expect_post_ev_mon_update {
check_added_monitors(node, 0);
}
Expand All @@ -3051,6 +3051,7 @@ pub fn expect_payment_sent<CM: AChannelManager, H: NodeHolder<CM = CM>>(
}
// We return the invoice because some test may want to check the invoice details.
let invoice;
let contact_info_result;
let mut path_events = Vec::new();
let expected_payment_id = match events[0] {
Event::PaymentSent {
Expand All @@ -3060,6 +3061,7 @@ pub fn expect_payment_sent<CM: AChannelManager, H: NodeHolder<CM = CM>>(
ref amount_msat,
ref fee_paid_msat,
ref bolt12_invoice,
ref contact_info,
} => {
assert_eq!(expected_payment_preimage, *payment_preimage);
assert_eq!(expected_payment_hash, *payment_hash);
Expand All @@ -3070,6 +3072,7 @@ pub fn expect_payment_sent<CM: AChannelManager, H: NodeHolder<CM = CM>>(
assert!(fee_paid_msat.is_some());
}
invoice = bolt12_invoice.clone();
contact_info_result = contact_info.clone();
payment_id.unwrap()
},
_ => panic!("Unexpected event"),
Expand All @@ -3087,7 +3090,7 @@ pub fn expect_payment_sent<CM: AChannelManager, H: NodeHolder<CM = CM>>(
}
}
}
(invoice, path_events)
(invoice, path_events, contact_info_result)
}

#[macro_export]
Expand Down Expand Up @@ -4119,28 +4122,28 @@ pub fn pass_claimed_payment_along_route(args: ClaimAlongRouteArgs) -> u64 {
}
pub fn claim_payment_along_route(
args: ClaimAlongRouteArgs,
) -> (Option<PaidBolt12Invoice>, Vec<Event>) {
) -> (Option<PaidBolt12Invoice>, Vec<Event>, Option<crate::events::ContactInfo>) {
let origin_node = args.origin_node;
let payment_preimage = args.payment_preimage;
let skip_last = args.skip_last;
let expected_total_fee_msat = do_claim_payment_along_route(args);
if !skip_last {
expect_payment_sent!(origin_node, payment_preimage, Some(expected_total_fee_msat))
} else {
(None, Vec::new())
(None, Vec::new(), None)
}
}

pub fn claim_payment<'a, 'b, 'c>(
origin_node: &Node<'a, 'b, 'c>, expected_route: &[&Node<'a, 'b, 'c>],
our_payment_preimage: PaymentPreimage,
) -> Option<PaidBolt12Invoice> {
claim_payment_along_route(ClaimAlongRouteArgs::new(
) -> (Option<PaidBolt12Invoice>, Option<crate::events::ContactInfo>) {
let result = claim_payment_along_route(ClaimAlongRouteArgs::new(
origin_node,
&[expected_route],
our_payment_preimage,
))
.0
));
(result.0, result.2)
}

pub const TEST_FINAL_CLTV: u32 = 70;
Expand Down
Loading
Loading