Skip to content

Commit 29e5bd6

Browse files
Add OfferId to Bolt12Invoice and tests for offer_id correctness
- Add an Option<OfferId> field to Bolt12Invoice to track the originating offer. - Compute the offer_id for invoices created from offers by extracting the offer TLV records and hashing them with the correct tag. - Expose a public offer_id() accessor on Bolt12Invoice. - Add tests to ensure the offer_id in the invoice matches the originating Offer, and that refund invoices have no offer_id. - All existing and new tests pass. This enables linking invoices to their originating offers in a robust and spec-compliant way. Signed-off-by: Vincenzo Palazzo <[email protected]>
1 parent 285a45c commit 29e5bd6

File tree

2 files changed

+94
-2
lines changed

2 files changed

+94
-2
lines changed

lightning/src/offers/invoice.rs

Lines changed: 90 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ use crate::offers::merkle::{
135135
};
136136
use crate::offers::nonce::Nonce;
137137
use crate::offers::offer::{
138-
Amount, ExperimentalOfferTlvStream, ExperimentalOfferTlvStreamRef, OfferTlvStream,
138+
Amount, ExperimentalOfferTlvStream, ExperimentalOfferTlvStreamRef, OfferId, OfferTlvStream,
139139
OfferTlvStreamRef, Quantity, EXPERIMENTAL_OFFER_TYPES, OFFER_TYPES,
140140
};
141141
use crate::offers::parse::{Bolt12ParseError, Bolt12SemanticError, ParsedMessage};
@@ -665,6 +665,21 @@ impl UnsignedBolt12Invoice {
665665
pub fn tagged_hash(&self) -> &TaggedHash {
666666
&self.tagged_hash
667667
}
668+
669+
/// Computes the offer ID if this invoice corresponds to an offer.
670+
fn compute_offer_id(&self) -> Option<OfferId> {
671+
match &self.contents {
672+
InvoiceContents::ForOffer { .. } => {
673+
// Extract offer TLV records from the invoice bytes
674+
let offer_tlv_stream = TlvStream::new(&self.bytes).range(OFFER_TYPES);
675+
let experimental_offer_tlv_stream = TlvStream::new(&self.experimental_bytes).range(EXPERIMENTAL_OFFER_TYPES);
676+
let combined_tlv_stream = offer_tlv_stream.chain(experimental_offer_tlv_stream);
677+
let tagged_hash = TaggedHash::from_tlv_stream("LDK Offer ID", combined_tlv_stream);
678+
Some(OfferId(tagged_hash.to_bytes()))
679+
},
680+
InvoiceContents::ForRefund { .. } => None,
681+
}
682+
}
668683
}
669684

670685
macro_rules! unsigned_invoice_sign_method { ($self: ident, $self_type: ty $(, $self_mut: tt)?) => {
@@ -686,6 +701,9 @@ macro_rules! unsigned_invoice_sign_method { ($self: ident, $self_type: ty $(, $s
686701
// Append the experimental bytes after the signature.
687702
$self.bytes.extend_from_slice(&$self.experimental_bytes);
688703

704+
// Compute offer_id before moving fields
705+
let offer_id = $self.compute_offer_id();
706+
689707
Ok(Bolt12Invoice {
690708
#[cfg(not(c_bindings))]
691709
bytes: $self.bytes,
@@ -700,6 +718,7 @@ macro_rules! unsigned_invoice_sign_method { ($self: ident, $self_type: ty $(, $s
700718
tagged_hash: $self.tagged_hash,
701719
#[cfg(c_bindings)]
702720
tagged_hash: $self.tagged_hash.clone(),
721+
offer_id,
703722
})
704723
}
705724
} }
@@ -734,6 +753,7 @@ pub struct Bolt12Invoice {
734753
contents: InvoiceContents,
735754
signature: Signature,
736755
tagged_hash: TaggedHash,
756+
offer_id: Option<OfferId>,
737757
}
738758

739759
/// The contents of an [`Bolt12Invoice`] for responding to either an [`Offer`] or a [`Refund`].
@@ -967,6 +987,11 @@ impl Bolt12Invoice {
967987
self.tagged_hash.as_digest().as_ref().clone()
968988
}
969989

990+
/// Returns the offer ID if this invoice corresponds to an offer.
991+
pub fn offer_id(&self) -> Option<OfferId> {
992+
self.offer_id
993+
}
994+
970995
/// Verifies that the invoice was for a request or refund created using the given key by
971996
/// checking the payer metadata from the invoice request.
972997
///
@@ -1622,7 +1647,12 @@ impl TryFrom<ParsedMessage<FullInvoiceTlvStream>> for Bolt12Invoice {
16221647
let pubkey = contents.fields().signing_pubkey;
16231648
merkle::verify_signature(&signature, &tagged_hash, pubkey)?;
16241649

1625-
Ok(Bolt12Invoice { bytes, contents, signature, tagged_hash })
1650+
let offer_tlv_stream = TlvStream::new(&bytes).range(OFFER_TYPES);
1651+
let experimental_offer_tlv_stream = TlvStream::new(&bytes).range(EXPERIMENTAL_OFFER_TYPES);
1652+
let combined_tlv_stream = offer_tlv_stream.chain(experimental_offer_tlv_stream);
1653+
let offer_tagged_hash = TaggedHash::from_tlv_stream("LDK Offer ID", combined_tlv_stream);
1654+
let offer_id = OfferId::from_tagged_hash(&offer_tagged_hash);
1655+
Ok(Bolt12Invoice { bytes, contents, signature, tagged_hash, offer_id: Some(offer_id) })
16261656
}
16271657
}
16281658

@@ -3556,4 +3586,62 @@ mod tests {
35563586
),
35573587
}
35583588
}
3589+
3590+
#[test]
3591+
fn invoice_offer_id_matches_offer_id() {
3592+
let expanded_key = ExpandedKey::new([42; 32]);
3593+
let entropy = FixedEntropy {};
3594+
let nonce = Nonce::from_entropy_source(&entropy);
3595+
let secp_ctx = Secp256k1::new();
3596+
let payment_id = PaymentId([1; 32]);
3597+
3598+
// Create an offer
3599+
let offer = OfferBuilder::new(recipient_pubkey())
3600+
.amount_msats(1000)
3601+
.build()
3602+
.unwrap();
3603+
3604+
// Get the offer ID
3605+
let offer_id = offer.id();
3606+
3607+
// Create an invoice request from the offer
3608+
let invoice_request = offer
3609+
.request_invoice(&expanded_key, nonce, &secp_ctx, payment_id)
3610+
.unwrap()
3611+
.build_and_sign()
3612+
.unwrap();
3613+
3614+
// Create an invoice from the invoice request
3615+
let invoice = invoice_request
3616+
.respond_with_no_std(payment_paths(), payment_hash(), now())
3617+
.unwrap()
3618+
.build()
3619+
.unwrap()
3620+
.sign(recipient_sign)
3621+
.unwrap();
3622+
3623+
// Verify that the invoice's offer_id matches the offer's id
3624+
assert_eq!(invoice.offer_id(), Some(offer_id));
3625+
}
3626+
3627+
#[test]
3628+
fn refund_invoice_has_no_offer_id() {
3629+
// Create a refund
3630+
let refund = RefundBuilder::new(vec![1; 32], payer_pubkey(), 1000)
3631+
.unwrap()
3632+
.build()
3633+
.unwrap();
3634+
3635+
// Create an invoice from the refund
3636+
let invoice = refund
3637+
.respond_with_no_std(payment_paths(), payment_hash(), recipient_pubkey(), now())
3638+
.unwrap()
3639+
.build()
3640+
.unwrap()
3641+
.sign(recipient_sign)
3642+
.unwrap();
3643+
3644+
// Verify that the refund invoice has no offer_id
3645+
assert_eq!(invoice.offer_id(), None);
3646+
}
35593647
}

lightning/src/offers/offer.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,10 @@ impl OfferId {
133133
let tagged_hash = TaggedHash::from_tlv_stream(Self::ID_TAG, tlv_stream);
134134
Self(tagged_hash.to_bytes())
135135
}
136+
137+
pub (crate) fn from_tagged_hash(tagged_hash: &TaggedHash) -> Self {
138+
Self(tagged_hash.to_bytes())
139+
}
136140
}
137141

138142
impl Borrow<[u8]> for OfferId {

0 commit comments

Comments
 (0)