Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
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
240 changes: 229 additions & 11 deletions lightning/src/offers/invoice_request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,10 @@ macro_rules! invoice_request_builder_methods { (
payer: PayerContents(metadata), offer, chain: None, amount_msats: None,
features: InvoiceRequestFeatures::empty(), quantity: None, payer_note: None,
offer_from_hrn: None,
contact_secret: None,
payer_offer: None,
payer_bip353_name: None,
payer_bip353_signature: None,
#[cfg(test)]
experimental_bar: None,
}
Expand Down Expand Up @@ -254,6 +258,84 @@ macro_rules! invoice_request_builder_methods { (
$return_value
}

/// Sets the contact secret for this payment, as defined in BLIP-0042.
///
/// The contact secret allows the recipient to identify the payer as a contact.
/// If a contact pair already exists between the payer and recipient, use
/// `with_derived_contact_secret` instead.
///
/// Successive calls to this method will override the previous setting.
pub fn with_contact_secret($($self_mut)* $self: $self_type, contact_secret: ContactSecret) -> $return_type {
$self.invoice_request.contact_secret = Some(contact_secret);
$return_value
}

/// Derives and sets a contact secret for this payment based on both the payer's and recipient's offers,
/// as defined in BLIP-0042.
///
/// This method implements the deterministic contact key generation algorithm
/// from BLIP-0042, creating a shared secret that identifies the contact pair.
///
/// Successive calls to this method will override the previous setting.
pub fn with_derived_contact_secret($($self_mut)* $self: $self_type, payer_offer: &Offer) -> $return_type {
let contact_secret = derive_contact_secret(payer_offer, $self.offer);
$self.invoice_request.contact_secret = Some(contact_secret);
$return_value
}

/// Sets the payer's BOLT12 offer to allow the recipient to pay them back.
///
/// This allows the recipient to add the payer as a contact and initiate
/// payments back to them in the future.
///
/// Successive calls to this method will override the previous setting.
pub fn with_payer_offer($($self_mut)* $self: $self_type, payer_offer: &Offer) -> $return_type {
let mut offer_bytes = Vec::new();
payer_offer.write(&mut offer_bytes).unwrap_or_default();
Copy link

Copilot AI Apr 24, 2025

Choose a reason for hiding this comment

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

Using unwrap_or_default when writing the payer offer may hide serialization errors. Consider handling the error explicitly to either propagate it or log a meaningful message.

Copilot uses AI. Check for mistakes.

$self.invoice_request.payer_offer = Some(offer_bytes);
$return_value
}

/// Sets the payer's BIP-353 name information for contact identification.
///
/// This allows the payer to reveal their BIP-353 name to allow contacts to pay them back.
///
/// Successive calls to this method will override the previous setting.
pub fn with_payer_bip353_name($($self_mut)* $self: $self_type, name: String, domain: String) -> $return_type {
$self.invoice_request.payer_bip353_name = Some(Bip353Name { name, domain });
$return_value
}

/// Signs the payment with a BIP-353 signature proving the payer owns the BIP-353 name.
///
/// This should only be called if `with_payer_bip353_name` has been used.
/// The signing key must be the private key of the offer issuer ID or blinded path node.
///
/// Successive calls to this method will override the previous setting.
pub fn sign_with_bip353_key<S: secp256k1::Signing>(
$($self_mut)* $self: $self_type,
signing_key: &Keypair,
secp_ctx: &Secp256k1<S>
) -> $return_type {
if let Some(bip353_name) = &$self.invoice_request.payer_bip353_name {
// Create a tagged hash for BIP-353 signature
use sha2::{Digest, Sha256};
let tag = "lightning/invoice_request/invreq_payer_bip353_signature";
let mut message = Vec::new();
message.extend_from_slice(tag.as_bytes());
message.extend_from_slice(bip353_name.name.as_bytes());
message.extend_from_slice(bip353_name.domain.as_bytes());

let message_hash = Sha256::digest(&message);
let message = secp256k1::Message::from_slice(&message_hash).unwrap();
let signature = secp_ctx.sign_schnorr_no_aux_rand(&message, signing_key);

$self.invoice_request.payer_bip353_signature = Some((signing_key.public_key(), signature));
}

$return_value
}

fn build_with_checks($($self_mut)* $self: $self_type) -> Result<
(UnsignedInvoiceRequest, Option<Keypair>, Option<&'b Secp256k1<$secp_context>>),
Bolt12SemanticError
Expand Down Expand Up @@ -636,6 +718,11 @@ pub(super) struct InvoiceRequestContentsWithoutPayerSigningPubkey {
quantity: Option<u64>,
payer_note: Option<String>,
offer_from_hrn: Option<HumanReadableName>,
// Contact-related fields
contact_secret: Option<ContactSecret>,
payer_offer: Option<Vec<u8>>,
payer_bip353_name: Option<Bip353Name>,
payer_bip353_signature: Option<(PublicKey, Signature)>,
#[cfg(test)]
experimental_bar: Option<u64>,
}
Expand Down Expand Up @@ -699,9 +786,42 @@ macro_rules! invoice_request_accessors { ($self: ident, $contents: expr) => {
}
} }

macro_rules! invoice_request_contact_accessors { ($self: ident, $contents: expr) => {
/// Returns the contact secret if present, allowing the recipient to identify
/// this payer as a contact based on a shared secret derived from both parties' offers.
///
/// This is part of the BLIP-0042 contacts implementation.
pub fn contact_secret(&$self) -> Option<&[u8]> {
$contents.contact_secret().map(|s| s.as_bytes())
}

/// Returns the payer's offer if included, which can be used to pay back to
/// the payer in future payments.
///
/// This is part of the BLIP-0042 contacts implementation.
pub fn payer_offer(&$self) -> Option<Offer> {
$contents.payer_offer().and_then(|bytes| Offer::try_from(bytes.to_vec()).ok())
}

/// Returns the payer's BIP-353 name if present.
///
/// This is part of the BLIP-0042 contacts implementation.
pub fn payer_bip353_name(&$self) -> Option<(&str, &str)> {
$contents.payer_bip353_name().map(|name| (name.name.as_str(), name.domain.as_str()))
}

/// Returns the BIP-353 signature if present, which proves ownership of the BIP-353 name.
///
/// This is part of the BLIP-0042 contacts implementation.
pub fn payer_bip353_signature(&$self) -> Option<(&PublicKey, &Signature)> {
$contents.payer_bip353_signature()
}
} }

impl UnsignedInvoiceRequest {
offer_accessors!(self, self.contents.inner.offer);
invoice_request_accessors!(self, self.contents);
invoice_request_contact_accessors!(self, self.contents);
}

macro_rules! invoice_request_respond_with_explicit_signing_pubkey_methods { (
Expand Down Expand Up @@ -851,6 +971,7 @@ macro_rules! invoice_request_verify_method {
impl InvoiceRequest {
offer_accessors!(self, self.contents.inner.offer);
invoice_request_accessors!(self, self.contents);
invoice_request_contact_accessors!(self, self.contents);
invoice_request_respond_with_explicit_signing_pubkey_methods!(
self,
self,
Expand All @@ -868,6 +989,7 @@ impl InvoiceRequest {
impl InvoiceRequest {
offer_accessors!(self, self.contents.inner.offer);
invoice_request_accessors!(self, self.contents);
invoice_request_contact_accessors!(self, self.contents);
invoice_request_respond_with_explicit_signing_pubkey_methods!(
self,
self,
Expand All @@ -894,6 +1016,7 @@ impl InvoiceRequest {
payer_tlv_stream,
offer_tlv_stream,
invoice_request_tlv_stream,
signature_tlv_stream,
experimental_offer_tlv_stream,
experimental_invoice_request_tlv_stream,
) = self.contents.as_tlv_stream();
Expand Down Expand Up @@ -964,6 +1087,7 @@ macro_rules! invoice_request_respond_with_derived_signing_pubkey_methods { (
impl VerifiedInvoiceRequest {
offer_accessors!(self, self.inner.contents.inner.offer);
invoice_request_accessors!(self, self.inner.contents);
invoice_request_contact_accessors!(self, self.inner.contents);
#[cfg(not(c_bindings))]
invoice_request_respond_with_explicit_signing_pubkey_methods!(
self,
Expand Down Expand Up @@ -1023,7 +1147,6 @@ impl InvoiceRequestContents {
},
Some(Amount::Currency { .. }) => None,
None => {
debug_assert!(false);
None
},
})
Expand All @@ -1050,13 +1173,78 @@ impl InvoiceRequestContents {
}

pub(super) fn offer_from_hrn(&self) -> &Option<HumanReadableName> {
&self.inner.offer_from_hrn
self.inner.offer_from_hrn
}

pub(super) fn contact_secret(&self) -> Option<&ContactSecret> {
self.inner.contact_secret.as_ref()
}

pub(super) fn payer_offer(&self) -> Option<&[u8]> {
self.inner.payer_offer.as_ref().map(|offer| offer.as_slice())
}

pub(super) fn payer_bip353_name(&self) -> Option<&Bip353Name> {
self.inner.payer_bip353_name.as_ref()
}

pub(super) fn payer_bip353_signature(&self) -> Option<(&PublicKey, &Signature)> {
self.inner.payer_bip353_signature.as_ref()
.map(|(pubkey, sig)| (pubkey, sig))
}

pub(super) fn as_tlv_stream(&self) -> PartialInvoiceRequestTlvStreamRef {
let (payer, offer, mut invoice_request, experimental_offer, experimental_invoice_request) =
self.inner.as_tlv_stream();
invoice_request.payer_id = Some(&self.payer_signing_pubkey);
let payer = PayerTlvStreamRef { metadata: self.payer.0.as_bytes() };

let (offer, experimental_offer) = self.offer.as_tlv_stream();

let features = {
if self.features == InvoiceRequestFeatures::empty() {
None
} else {
Some(&self.features)
}
};

let invoice_request = InvoiceRequestTlvStreamRef {
chain: self.chain.as_ref(),
amount: self.amount_msats,
features,
quantity: self.quantity,
payer_id: None,
payer_note: self.payer_note.as_ref(),
offer_from_hrn: self.offer_from_hrn.as_ref(),
paths: None,
};

// Handle contact-related fields
let mut experimental_invoice_request = ExperimentalInvoiceRequestTlvStreamRef {
contact_secret: self.contact_secret.as_ref().map(|secret| secret.as_bytes()),
payer_offer: self.payer_offer.as_ref().map(|offer| offer.as_slice()),
payer_bip353_name: None,
payer_bip353_signature: None,
#[cfg(test)]
experimental_bar: self.experimental_bar,
};

// Serialize BIP-353 name if present
if let Some(bip353_name) = &self.payer_bip353_name {
let mut name_bytes = Vec::new();
name_bytes.push(bip353_name.name.len() as u8);
name_bytes.extend_from_slice(bip353_name.name.as_bytes());
name_bytes.push(bip353_name.domain.len() as u8);
name_bytes.extend_from_slice(bip353_name.domain.as_bytes());
experimental_invoice_request.payer_bip353_name = Some(&name_bytes);
}

// Serialize BIP-353 signature if present
if let Some((pubkey, signature)) = &self.payer_bip353_signature {
let mut sig_bytes = Vec::new();
sig_bytes.extend_from_slice(&pubkey.serialize());
sig_bytes.extend_from_slice(&signature.as_ref());
experimental_invoice_request.payer_bip353_signature = Some(&sig_bytes);
}

(payer, offer, invoice_request, experimental_offer, experimental_invoice_request)
}
}
Expand Down Expand Up @@ -1099,8 +1287,12 @@ impl InvoiceRequestContentsWithoutPayerSigningPubkey {
};

let experimental_invoice_request = ExperimentalInvoiceRequestTlvStreamRef {
contact_secret: self.contact_secret.as_ref().map(|secret| secret.as_bytes()),
payer_offer: self.payer_offer.as_ref().map(|offer| offer.as_slice()),
payer_bip353_name: None,
payer_bip353_signature: None,
#[cfg(test)]
experimental_bar: self.experimental_bar,
experimental_bar,
};

(payer, offer, invoice_request, experimental_offer, experimental_invoice_request)
Expand Down Expand Up @@ -1165,18 +1357,27 @@ pub(super) const EXPERIMENTAL_INVOICE_REQUEST_TYPES: core::ops::Range<u64> =
#[cfg(not(test))]
tlv_stream!(
ExperimentalInvoiceRequestTlvStream,
ExperimentalInvoiceRequestTlvStreamRef,
ExperimentalInvoiceRequestTlvStreamRef<'a>,
EXPERIMENTAL_INVOICE_REQUEST_TYPES,
{
// When adding experimental TLVs, update EXPERIMENTAL_TLV_ALLOCATION_SIZE accordingly in
// UnsignedInvoiceRequest::new to avoid unnecessary allocations.
// Contact-related TLV fields defined in BLIP-0042
(2000001729, contact_secret: [u8; 32]),
(2000001731, payer_offer: (Vec<u8>, WithoutLength)),
(2000001733, payer_bip353_name: (Vec<u8>, WithoutLength)),
(2000001735, payer_bip353_signature: (Vec<u8>, WithoutLength)),
}
);

#[cfg(test)]
tlv_stream!(
ExperimentalInvoiceRequestTlvStream, ExperimentalInvoiceRequestTlvStreamRef,
ExperimentalInvoiceRequestTlvStream,
ExperimentalInvoiceRequestTlvStreamRef<'a>,
EXPERIMENTAL_INVOICE_REQUEST_TYPES, {
// Contact-related TLV fields defined in BLIP-0042
(2000001729, contact_secret: [u8; 32]),
(2000001731, payer_offer: (Vec<u8>, WithoutLength)),
(2000001733, payer_bip353_name: (Vec<u8>, WithoutLength)),
(2000001735, payer_bip353_signature: (Vec<u8>, WithoutLength)),
(2_999_999_999, experimental_bar: (u64, HighZeroBytesDroppedBigSize)),
}
);
Expand Down Expand Up @@ -1311,6 +1512,10 @@ impl TryFrom<PartialInvoiceRequestTlvStream> for InvoiceRequestContents {
},
experimental_offer_tlv_stream,
ExperimentalInvoiceRequestTlvStream {
contact_secret,
payer_offer,
payer_bip353_name,
payer_bip353_signature,
#[cfg(test)]
experimental_bar,
},
Expand Down Expand Up @@ -1354,6 +1559,20 @@ impl TryFrom<PartialInvoiceRequestTlvStream> for InvoiceRequestContents {
quantity,
payer_note,
offer_from_hrn,
contact_secret: contact_secret.map(ContactSecret::from),
payer_offer,
payer_bip353_name: payer_bip353_name.map(|bytes| {
let name_len = bytes[0] as usize;
let name = String::from_utf8(bytes[1..1 + name_len].to_vec()).unwrap();
let domain_len = bytes[1 + name_len] as usize;
let domain = String::from_utf8(bytes[2 + name_len..2 + name_len + domain_len].to_vec()).unwrap();
Bip353Name { name, domain }
}),
payer_bip353_signature: payer_bip353_signature.map(|bytes| {
let pubkey = PublicKey::from_slice(&bytes[..33]).unwrap();
let signature = Signature::from_slice(&bytes[33..]).unwrap();
(pubkey, signature)
}),
Copy link

Copilot AI Apr 24, 2025

Choose a reason for hiding this comment

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

The deserialization of payer_bip353_name (and similarly payer_bip353_signature) uses unwrap(), which may cause a panic on malformed input. Consider adding proper error handling to safely manage conversion errors.

Suggested change
let name = String::from_utf8(bytes[1..1 + name_len].to_vec()).unwrap();
let domain_len = bytes[1 + name_len] as usize;
let domain = String::from_utf8(bytes[2 + name_len..2 + name_len + domain_len].to_vec()).unwrap();
Bip353Name { name, domain }
}),
payer_bip353_signature: payer_bip353_signature.map(|bytes| {
let pubkey = PublicKey::from_slice(&bytes[..33]).unwrap();
let signature = Signature::from_slice(&bytes[33..]).unwrap();
(pubkey, signature)
}),
let name = String::from_utf8(bytes[1..1 + name_len].to_vec())
.map_err(|_| Bolt12SemanticError::InvalidPayerBip353Name)?;
let domain_len = bytes[1 + name_len] as usize;
let domain = String::from_utf8(bytes[2 + name_len..2 + name_len + domain_len].to_vec())
.map_err(|_| Bolt12SemanticError::InvalidPayerBip353Name)?;
Ok(Bip353Name { name, domain })
}).transpose()?,
payer_bip353_signature: payer_bip353_signature.map(|bytes| {
let pubkey = PublicKey::from_slice(&bytes[..33])
.map_err(|_| Bolt12SemanticError::InvalidPayerBip353Signature)?;
let signature = Signature::from_slice(&bytes[33..])
.map_err(|_| Bolt12SemanticError::InvalidPayerBip353Signature)?;
Ok((pubkey, signature))
}).transpose()?,

Copilot uses AI. Check for mistakes.

#[cfg(test)]
experimental_bar,
},
Expand Down Expand Up @@ -1712,7 +1931,6 @@ mod tests {

let invoice_request = OfferBuilder::new(recipient_pubkey())
.amount_msats(1000)
.chain(Network::Testnet)
.build()
.unwrap()
.request_invoice(&expanded_key, nonce, &secp_ctx, payment_id)
Expand Down
Loading