Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
2 changes: 2 additions & 0 deletions fuzz/src/invoice_request_deser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ fn build_response<T: secp256k1::Signing + secp256k1::Verification>(
.payer_note()
.map(|s| UntrustedString(s.to_string())),
human_readable_name: None,
contact_secret: None,
payer_offer: None,
}
};

Expand Down
79 changes: 79 additions & 0 deletions lightning/src/ln/channelmanager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,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 @@ -773,6 +774,34 @@ 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
Copy Markdown
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_secrets: Option<ContactSecrets>,
/// A custom payer offer to include in the invoice request for BLIP-42 contact management.
///
/// If provided, this offer will be included in the invoice request, allowing the recipient to
/// contact you back. If `None`, **no payer offer will be included** in the invoice request.
///
/// You can create custom offers using [`OffersMessageFlow::create_compact_offer_builder`]:
/// - Pass `None` for no blinded path (smallest size, ~70 bytes)
/// - Pass `Some(intro_node_id)` for a single blinded path (~200 bytes)
///
/// # Example
/// ```rust,ignore
/// // Include a compact offer with a single blinded path
/// let payer_offer = flow.create_compact_offer_builder(
/// &entropy_source,
/// Some(trusted_peer_pubkey)
/// )?.build()?;
///
/// let params = OptionalOfferPaymentParams {
/// payer_offer: Some(payer_offer),
/// ..Default::default()
/// };
/// ```
///
/// [`OffersMessageFlow::create_compact_offer_builder`]: crate::offers::flow::OffersMessageFlow::create_compact_offer_builder
pub payer_offer: Option<Offer>,
Comment on lines +797 to +804
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Design concern: No size validation on payer_offer

The Offer passed here gets serialized into both:

  1. The invoice request's experimental TLV (the onion message carrying the invreq)
  2. InvoiceRequestFieldsBolt12OfferContext → the final hop's encrypted payload in every blinded payment path of the resulting invoice

For (2), the HTLC onion packet has a ~1300-byte total payload budget across all hops. Adding 70-200+ bytes for a compact offer (plus TLV overhead) to the final hop's encrypted data reduces the available space for intermediate hops, limiting route length.

Consider adding a size check (e.g., offer.as_ref().len() <= MAX_PAYER_OFFER_SIZE) and returning an error if the offer is too large, or at least documenting the size constraint so callers know they must use a compact offer.

}

impl Default for OptionalOfferPaymentParams {
Expand All @@ -784,6 +813,8 @@ impl Default for OptionalOfferPaymentParams {
retry_strategy: Retry::Timeout(core::time::Duration::from_secs(2)),
#[cfg(not(feature = "std"))]
retry_strategy: Retry::Attempts(3),
contact_secrets: None,
payer_offer: None,
}
}
}
Expand Down Expand Up @@ -14619,6 +14650,33 @@ macro_rules! create_offer_builder { ($self: ident, $builder: ty) => {

Ok(builder.into())
}

/// Creates a compact [`OfferBuilder`] suitable for BLIP-42's `payer_offer` field.
///
/// This creates an offer with minimal size by either:
/// - Having no blinded paths when `intro_node_id` is `None` (for public nodes)
/// - Having a single one-hop blinded path when `intro_node_id` is `Some` (for private nodes)
///
/// The compact format is ideal for encoding in invoice request fields where space is limited.
///
/// # Privacy
///
/// Uses a derived signing pubkey in the offer for recipient privacy.
///
/// # Errors
///
/// Errors if a blinded path cannot be created when `intro_node_id` is provided.
///
/// [`Offer`]: crate::offers::offer::Offer
pub fn create_compact_offer_builder(
&$self, intro_node_id: Option<PublicKey>,
) -> Result<$builder, Bolt12SemanticError> {
let builder = $self.flow.create_compact_offer_builder(
&$self.entropy_source, intro_node_id
)?;

Ok(builder.into())
}
} }

macro_rules! create_refund_builder { ($self: ident, $builder: ty) => {
Expand Down Expand Up @@ -14854,6 +14912,8 @@ impl<
payment_id,
None,
create_pending_payment_fn,
optional_params.contact_secrets,
optional_params.payer_offer,
)
}

Expand Down Expand Up @@ -14883,6 +14943,8 @@ impl<
payment_id,
Some(offer.hrn),
create_pending_payment_fn,
optional_params.contact_secrets,
optional_params.payer_offer,
)
}

Expand Down Expand Up @@ -14925,6 +14987,8 @@ impl<
payment_id,
None,
create_pending_payment_fn,
optional_params.contact_secrets,
optional_params.payer_offer,
)
}

Expand All @@ -14933,6 +14997,7 @@ impl<
&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>, payer_offer: Option<Offer>,
) -> Result<(), Bolt12SemanticError> {
let entropy = &self.entropy_source;
let nonce = Nonce::from_entropy_source(entropy);
Expand All @@ -14958,6 +15023,20 @@ impl<
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
};

// Add payer offer only if provided by the user.
// If the user explicitly wants to include an offer, they should provide it via payer_offer parameter.
let builder = if let Some(offer) = payer_offer {
builder.payer_offer(&offer)
} else {
builder
};

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

Expand Down
14 changes: 14 additions & 0 deletions lightning/src/ln/offers_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -727,6 +727,8 @@ fn creates_and_pays_for_offer_using_two_hop_blinded_path() {
quantity: None,
payer_note_truncated: None,
human_readable_name: None,
contact_secret: None,
payer_offer: None,
},
});
assert_eq!(invoice_request.amount_msats(), Some(10_000_000));
Expand Down Expand Up @@ -885,6 +887,8 @@ fn creates_and_pays_for_offer_using_one_hop_blinded_path() {
quantity: None,
payer_note_truncated: None,
human_readable_name: None,
contact_secret: None,
payer_offer: None,
},
});
assert_eq!(invoice_request.amount_msats(), Some(10_000_000));
Expand Down Expand Up @@ -1006,6 +1010,8 @@ fn pays_for_offer_without_blinded_paths() {
quantity: None,
payer_note_truncated: None,
human_readable_name: None,
contact_secret: None,
payer_offer: None,
},
});

Expand Down Expand Up @@ -1274,6 +1280,8 @@ fn creates_and_pays_for_offer_with_retry() {
quantity: None,
payer_note_truncated: None,
human_readable_name: None,
contact_secret: None,
payer_offer: None,
},
});
assert_eq!(invoice_request.amount_msats(), Some(10_000_000));
Expand Down Expand Up @@ -1339,6 +1347,8 @@ fn pays_bolt12_invoice_asynchronously() {
quantity: None,
payer_note_truncated: None,
human_readable_name: None,
contact_secret: None,
payer_offer: None,
},
});

Expand Down Expand Up @@ -1436,6 +1446,8 @@ fn creates_offer_with_blinded_path_using_unannounced_introduction_node() {
quantity: None,
payer_note_truncated: None,
human_readable_name: None,
contact_secret: None,
payer_offer: None,
},
});
assert_ne!(invoice_request.payer_signing_pubkey(), bob_id);
Expand Down Expand Up @@ -2647,6 +2659,8 @@ fn creates_and_pays_for_phantom_offer() {
quantity: None,
payer_note_truncated: None,
human_readable_name: None,
contact_secret: None,
payer_offer: None,
},
});

Expand Down
Loading
Loading