Skip to content
Open
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.
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 contacts = match contacts {
None => ContactSecrets::new(self.entropy_source.get_secure_random_bytes()),
Some(c) => c,
};
let builder = builder.contact_secrets(contacts.clone());
// Create a minimal offer for BLIP-42 contact exchange (just node_id, no description/paths)
// TODO: Create a better minimal offer with a single blinded path hop for privacy,
// while keeping the size small enough to fit in the onion packet.
let payer_offer = self.create_offer_builder()?.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