Skip to content

Commit 4e4ef84

Browse files
blip42: Add BIP-353 signature support for invoice requests
Implements the BIP-353 signature mechanism as specified in BLIP-42 for proving ownership of a human-readable name when sending invoice requests. Changes: - Add TLV type 2000001735 for invreq_payer_bip_353_signature - Add BIP353_SIGNATURE_TAG constant for tagged hash computation - Add bip353_name() builder method to set the payer's BIP-353 name - Add add_bip353_signature() method to compute and add the signature The signature is computed over only the invoice_request TLVs (types 80-159, 160-239) plus experimental invoice_request TLVs (excluding the signature itself), as specified in BLIP-42. The signature is encoded as [33 bytes pubkey][64 bytes schnorr signature]. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent f562577 commit 4e4ef84

File tree

3 files changed

+126
-1
lines changed

3 files changed

+126
-1
lines changed

lightning/src/offers/invoice.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2019,6 +2019,7 @@ mod tests {
20192019
invreq_contact_secret: None,
20202020
invreq_payer_offer: None,
20212021
invreq_payer_bip_353_name: None,
2022+
invreq_payer_bip_353_signature: None,
20222023
},
20232024
ExperimentalInvoiceTlvStreamRef { experimental_baz: None },
20242025
),
@@ -2127,6 +2128,7 @@ mod tests {
21272128
invreq_contact_secret: None,
21282129
invreq_payer_offer: None,
21292130
invreq_payer_bip_353_name: None,
2131+
invreq_payer_bip_353_signature: None,
21302132
},
21312133
ExperimentalInvoiceTlvStreamRef { experimental_baz: None },
21322134
),

lightning/src/offers/invoice_request.rs

Lines changed: 121 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,9 @@ use crate::prelude::*;
110110
/// Tag for the hash function used when signing an [`InvoiceRequest`]'s merkle root.
111111
pub const SIGNATURE_TAG: &'static str = concat!("lightning", "invoice_request", "signature");
112112

113+
/// Tag for the hash function used when signing a BIP-353 address in an [`InvoiceRequest`].
114+
pub const BIP353_SIGNATURE_TAG: &'static str = concat!("lightning", "invoice_request", "invreq_payer_bip_353_signature");
115+
113116
pub(super) const IV_BYTES: &[u8; IV_LEN] = b"LDK Invreq ~~~~~";
114117

115118
/// Builds an [`InvoiceRequest`] from an [`Offer`] for the "offer to be paid" flow.
@@ -188,6 +191,7 @@ macro_rules! invoice_request_builder_methods { (
188191
payer: PayerContents(metadata), offer, chain: None, amount_msats: None,
189192
features: InvoiceRequestFeatures::empty(), quantity: None, payer_note: None,
190193
offer_from_hrn: None, invreq_contact_secret: None, invreq_payer_offer: None,
194+
invreq_payer_bip_353_name: None, invreq_payer_bip_353_signature: None,
191195
#[cfg(test)]
192196
experimental_bar: None,
193197
}
@@ -279,6 +283,20 @@ macro_rules! invoice_request_builder_methods { (
279283
$return_value
280284
}
281285

286+
/// Sets the BIP-353 human-readable name for the payer.
287+
///
288+
/// This will include the serialized HumanReadableName in the invoice request.
289+
/// When set along with an offer's signing key, a BIP-353 signature will be
290+
/// created over the invoice request as specified in BLIP-42.
291+
///
292+
/// Successive calls to this method will override the previous setting.
293+
pub fn bip353_name($($self_mut)* $self: $self_type, name: &HumanReadableName) -> $return_type {
294+
let mut bytes = Vec::new();
295+
name.write(&mut bytes).unwrap();
296+
$self.invoice_request.invreq_payer_bip_353_name = Some(bytes);
297+
$return_value
298+
}
299+
282300
fn build_with_checks($($self_mut)* $self: $self_type) -> Result<
283301
(UnsignedInvoiceRequest, Option<Keypair>, Option<&'b Secp256k1<$secp_context>>),
284302
Bolt12SemanticError
@@ -549,6 +567,99 @@ impl UnsignedInvoiceRequest {
549567
pub fn tagged_hash(&self) -> &TaggedHash {
550568
&self.tagged_hash
551569
}
570+
571+
/// Adds a BIP-353 signature to the invoice request if a BIP-353 name is present.
572+
///
573+
/// This method should be called after creating the unsigned invoice request and before
574+
/// calling `sign()`. The signature is created by signing the invoice request TLV stream
575+
/// (excluding the top-level signature and the BIP-353 signature itself) with the provided
576+
/// keypair, which should be the signing key for the payer's own offer.
577+
///
578+
/// The signature proves ownership of the BIP-353 address by demonstrating possession of
579+
/// the corresponding offer's signing key.
580+
pub(super) fn add_bip353_signature<C: secp256k1::Signing>(
581+
&mut self, offer_keypair: &Keypair, secp_ctx: &Secp256k1<C>
582+
) -> Result<(), ()> {
583+
// Only add signature if BIP-353 name is present
584+
if self.contents.inner.invreq_payer_bip_353_name.is_none() {
585+
return Ok(());
586+
}
587+
588+
// Per BLIP-42 specification:
589+
// The BIP-353 signature is computed over the invoice_request TLV stream ONLY.
590+
// This means:
591+
// - EXCLUDE payer TLVs (type 0-7)
592+
// - EXCLUDE offer TLVs (type 1-79, 80-159)
593+
// - INCLUDE invoice_request TLVs (type 80-159, 160-239)
594+
// - INCLUDE experimental invoice_request TLVs (type 2000001729, 2000001731, 2000001733)
595+
// - EXCLUDE the BIP-353 signature itself (type 2000001735)
596+
// - EXCLUDE the main signature (type 240)
597+
//
598+
// The signature tag is: "lightning" || "invoice_request" || "invreq_payer_bip_353_signature"
599+
600+
// Build the TLV stream containing ONLY invoice_request fields (excluding signature)
601+
let (
602+
_payer_tlv_stream,
603+
_offer_tlv_stream,
604+
invoice_request_tlv_stream,
605+
_experimental_offer_tlv_stream,
606+
mut experimental_invoice_request_tlv_stream,
607+
) = self.contents.as_tlv_stream();
608+
609+
// Clear the signature field to exclude it from the hash
610+
experimental_invoice_request_tlv_stream.invreq_payer_bip_353_signature = None;
611+
612+
// Build the bytes to sign: only invoice_request TLVs + experimental invoice_request TLVs
613+
let mut invreq_bytes_to_sign = Vec::new();
614+
invoice_request_tlv_stream.write(&mut invreq_bytes_to_sign).unwrap();
615+
experimental_invoice_request_tlv_stream.write(&mut invreq_bytes_to_sign).unwrap();
616+
617+
// Create the tagged hash for BIP-353 signature
618+
let tlv_stream = TlvStream::new(&invreq_bytes_to_sign);
619+
let tagged_hash = TaggedHash::from_tlv_stream(BIP353_SIGNATURE_TAG, tlv_stream);
620+
621+
// Sign the hash
622+
let msg = tagged_hash.as_digest();
623+
let signature = secp_ctx.sign_schnorr_no_aux_rand(msg, offer_keypair);
624+
625+
// Encode as [33 bytes pubkey][64 bytes signature]
626+
let pubkey = offer_keypair.public_key();
627+
let mut signature_bytes = Vec::with_capacity(97);
628+
signature_bytes.extend_from_slice(&pubkey.serialize());
629+
signature_bytes.extend_from_slice(&signature[..]);
630+
631+
// Update the contents with the signature
632+
self.contents.inner.invreq_payer_bip_353_signature = Some(signature_bytes.clone());
633+
634+
// Rebuild the experimental bytes with the signature included.
635+
// The experimental_bytes contains: [experimental offer TLVs] + [experimental invoice request TLVs]
636+
// We need to preserve the experimental offer TLVs and update only the invoice request part.
637+
638+
// Extract experimental offer TLVs from current experimental_bytes
639+
let mut new_experimental_bytes = Vec::new();
640+
for record in TlvStream::new(&self.experimental_bytes).range(EXPERIMENTAL_OFFER_TYPES) {
641+
record.write(&mut new_experimental_bytes).unwrap();
642+
}
643+
644+
// Write experimental invoice request TLVs with the signature
645+
let experimental_invoice_request_tlv_stream_with_sig = ExperimentalInvoiceRequestTlvStreamRef {
646+
invreq_contact_secret: self.contents.inner.invreq_contact_secret.as_ref(),
647+
invreq_payer_offer: self.contents.inner.invreq_payer_offer.as_ref(),
648+
invreq_payer_bip_353_name: self.contents.inner.invreq_payer_bip_353_name.as_ref(),
649+
invreq_payer_bip_353_signature: Some(&signature_bytes),
650+
#[cfg(test)]
651+
experimental_bar: self.contents.inner.experimental_bar,
652+
};
653+
experimental_invoice_request_tlv_stream_with_sig.write(&mut new_experimental_bytes).unwrap();
654+
655+
self.experimental_bytes = new_experimental_bytes;
656+
657+
// Recompute the main tagged hash now that we've added the BIP-353 signature
658+
let tlv_stream = TlvStream::new(&self.bytes).chain(TlvStream::new(&self.experimental_bytes));
659+
self.tagged_hash = TaggedHash::from_tlv_stream(SIGNATURE_TAG, tlv_stream);
660+
661+
Ok(())
662+
}
552663
}
553664

554665
macro_rules! unsigned_invoice_request_sign_method { (
@@ -716,6 +827,8 @@ pub(super) struct InvoiceRequestContentsWithoutPayerSigningPubkey {
716827
offer_from_hrn: Option<HumanReadableName>,
717828
invreq_contact_secret: Option<[u8; 32]>,
718829
invreq_payer_offer: Option<Vec<u8>>,
830+
invreq_payer_bip_353_name: Option<Vec<u8>>,
831+
invreq_payer_bip_353_signature: Option<Vec<u8>>,
719832
#[cfg(test)]
720833
experimental_bar: Option<u64>,
721834
}
@@ -1281,7 +1394,8 @@ impl InvoiceRequestContentsWithoutPayerSigningPubkey {
12811394
let experimental_invoice_request = ExperimentalInvoiceRequestTlvStreamRef {
12821395
invreq_contact_secret: self.invreq_contact_secret.as_ref(),
12831396
invreq_payer_offer: self.invreq_payer_offer.as_ref(),
1284-
invreq_payer_bip_353_name: None,
1397+
invreq_payer_bip_353_name: self.invreq_payer_bip_353_name.as_ref(),
1398+
invreq_payer_bip_353_signature: self.invreq_payer_bip_353_signature.as_ref(),
12851399
#[cfg(test)]
12861400
experimental_bar: self.experimental_bar,
12871401
};
@@ -1374,6 +1488,7 @@ tlv_stream!(
13741488
(2_000_001_729, invreq_contact_secret: [u8; 32]),
13751489
(2_000_001_731, invreq_payer_offer: (Vec<u8>, WithoutLength)),
13761490
(2_000_001_733, invreq_payer_bip_353_name: (Vec<u8>, WithoutLength)),
1491+
(2_000_001_735, invreq_payer_bip_353_signature: (Vec<u8>, WithoutLength)),
13771492
// When adding experimental TLVs, update EXPERIMENTAL_TLV_ALLOCATION_SIZE accordingly in
13781493
// UnsignedInvoiceRequest::new to avoid unnecessary allocations.
13791494
}
@@ -1386,6 +1501,7 @@ tlv_stream!(
13861501
(2_000_001_729, invreq_contact_secret: [u8; 32]),
13871502
(2_000_001_731, invreq_payer_offer: (Vec<u8>, WithoutLength)),
13881503
(2_000_001_733, invreq_payer_bip_353_name: (Vec<u8>, WithoutLength)),
1504+
(2_000_001_735, invreq_payer_bip_353_signature: (Vec<u8>, WithoutLength)),
13891505
(2_999_999_999, experimental_bar: (u64, HighZeroBytesDroppedBigSize)),
13901506
}
13911507
);
@@ -1523,6 +1639,7 @@ impl TryFrom<PartialInvoiceRequestTlvStream> for InvoiceRequestContents {
15231639
invreq_contact_secret,
15241640
invreq_payer_offer,
15251641
invreq_payer_bip_353_name: _,
1642+
invreq_payer_bip_353_signature: _,
15261643
#[cfg(test)]
15271644
experimental_bar,
15281645
},
@@ -1568,6 +1685,8 @@ impl TryFrom<PartialInvoiceRequestTlvStream> for InvoiceRequestContents {
15681685
offer_from_hrn,
15691686
invreq_contact_secret,
15701687
invreq_payer_offer,
1688+
invreq_payer_bip_353_name: None,
1689+
invreq_payer_bip_353_signature: None,
15711690
#[cfg(test)]
15721691
experimental_bar,
15731692
},
@@ -1773,6 +1892,7 @@ mod tests {
17731892
invreq_contact_secret: None,
17741893
invreq_payer_offer: None,
17751894
invreq_payer_bip_353_name: None,
1895+
invreq_payer_bip_353_signature: None,
17761896
experimental_bar: None,
17771897
},
17781898
),

lightning/src/offers/refund.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -819,6 +819,7 @@ impl RefundContents {
819819
invreq_contact_secret: None,
820820
invreq_payer_offer: None,
821821
invreq_payer_bip_353_name: None,
822+
invreq_payer_bip_353_signature: None,
822823
#[cfg(test)]
823824
experimental_bar: self.experimental_bar,
824825
};
@@ -940,6 +941,7 @@ impl TryFrom<RefundTlvStream> for RefundContents {
940941
invreq_contact_secret: _,
941942
invreq_payer_offer: _,
942943
invreq_payer_bip_353_name: _,
944+
invreq_payer_bip_353_signature: _,
943945
#[cfg(test)]
944946
experimental_bar,
945947
},
@@ -1130,6 +1132,7 @@ mod tests {
11301132
invreq_contact_secret: None,
11311133
invreq_payer_offer: None,
11321134
invreq_payer_bip_353_name: None,
1135+
invreq_payer_bip_353_signature: None,
11331136
experimental_bar: None,
11341137
},
11351138
),

0 commit comments

Comments
 (0)