Skip to content

Commit

Permalink
Parse experimental invoice TLV records
Browse files Browse the repository at this point in the history
The BOLT12 spec defines an experimental TLV range that is allowed in
offer and invoice_request messages. The remaining TLV-space is for
experimental use in invoice messages. Allow this range when parsing an
invoice and include it when signing one.
  • Loading branch information
jkczyz committed Sep 16, 2024
1 parent ad638a5 commit 575433d
Show file tree
Hide file tree
Showing 4 changed files with 129 additions and 44 deletions.
85 changes: 57 additions & 28 deletions lightning/src/offers/invoice.rs
Original file line number Diff line number Diff line change
Expand Up @@ -505,7 +505,9 @@ impl UnsignedBolt12Invoice {
record.write(&mut bytes).unwrap();
}

let (_, _, _, invoice_tlv_stream, _, _) = contents.as_tlv_stream();
let (_, _, _, invoice_tlv_stream, _, _, experimental_invoice_tlv_stream) =
contents.as_tlv_stream();

invoice_tlv_stream.write(&mut bytes).unwrap();

let mut experimental_bytes = Vec::new();
Expand All @@ -514,6 +516,8 @@ impl UnsignedBolt12Invoice {
record.write(&mut experimental_bytes).unwrap();
}

experimental_invoice_tlv_stream.write(&mut experimental_bytes).unwrap();

let tlv_stream = TlvStream::new(&bytes).chain(TlvStream::new(&experimental_bytes));
let tagged_hash = TaggedHash::from_tlv_stream(SIGNATURE_TAG, tlv_stream);

Expand Down Expand Up @@ -871,14 +875,15 @@ impl Bolt12Invoice {
let (
payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream,
experimental_offer_tlv_stream, experimental_invoice_request_tlv_stream,
experimental_invoice_tlv_stream,
) = self.contents.as_tlv_stream();
let signature_tlv_stream = SignatureTlvStreamRef {
signature: Some(&self.signature),
};
(
payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream,
signature_tlv_stream, experimental_offer_tlv_stream,
experimental_invoice_request_tlv_stream,
experimental_invoice_request_tlv_stream, experimental_invoice_tlv_stream,
)
}

Expand Down Expand Up @@ -1139,9 +1144,12 @@ impl InvoiceContents {
InvoiceContents::ForOffer { invoice_request, .. } => invoice_request.as_tlv_stream(),
InvoiceContents::ForRefund { refund, .. } => refund.as_tlv_stream(),
};
let invoice = self.fields().as_tlv_stream();
let (invoice, experimental_invoice) = self.fields().as_tlv_stream();

(payer, offer, invoice_request, invoice, experimental_offer, experimental_invoice_request)
(
payer, offer, invoice_request, invoice, experimental_offer,
experimental_invoice_request, experimental_invoice,
)
}
}

Expand Down Expand Up @@ -1189,24 +1197,27 @@ pub(super) fn filter_fallbacks(
}

impl InvoiceFields {
fn as_tlv_stream(&self) -> InvoiceTlvStreamRef {
fn as_tlv_stream(&self) -> (InvoiceTlvStreamRef, ExperimentalInvoiceTlvStreamRef) {
let features = {
if self.features == Bolt12InvoiceFeatures::empty() { None }
else { Some(&self.features) }
};

InvoiceTlvStreamRef {
paths: Some(Iterable(self.payment_paths.iter().map(|path| path.inner_blinded_path()))),
blindedpay: Some(Iterable(self.payment_paths.iter().map(|path| &path.payinfo))),
created_at: Some(self.created_at.as_secs()),
relative_expiry: self.relative_expiry.map(|duration| duration.as_secs() as u32),
payment_hash: Some(&self.payment_hash),
amount: Some(self.amount_msats),
fallbacks: self.fallbacks.as_ref(),
features,
node_id: Some(&self.signing_pubkey),
message_paths: None,
}
(
InvoiceTlvStreamRef {
paths: Some(Iterable(self.payment_paths.iter().map(|path| path.inner_blinded_path()))),
blindedpay: Some(Iterable(self.payment_paths.iter().map(|path| &path.payinfo))),
created_at: Some(self.created_at.as_secs()),
relative_expiry: self.relative_expiry.map(|duration| duration.as_secs() as u32),
payment_hash: Some(&self.payment_hash),
amount: Some(self.amount_msats),
fallbacks: self.fallbacks.as_ref(),
features,
node_id: Some(&self.signing_pubkey),
message_paths: None,
},
ExperimentalInvoiceTlvStreamRef {},
)
}
}

Expand Down Expand Up @@ -1244,11 +1255,13 @@ impl TryFrom<Vec<u8>> for UnsignedBolt12Invoice {
let (
payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream,
experimental_offer_tlv_stream, experimental_invoice_request_tlv_stream,
experimental_invoice_tlv_stream,
) = tlv_stream;
let contents = InvoiceContents::try_from(
(
payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream,
experimental_offer_tlv_stream, experimental_invoice_request_tlv_stream,
experimental_invoice_tlv_stream,
)
)?;

Expand Down Expand Up @@ -1291,6 +1304,13 @@ tlv_stream!(InvoiceTlvStream, InvoiceTlvStreamRef<'a>, INVOICE_TYPES, {
(236, message_paths: (Vec<BlindedMessagePath>, WithoutLength)),
});

/// Valid type range for experimental invoice TLV records.
const EXPERIMENTAL_INVOICE_TYPES: core::ops::RangeFrom<u64> = 3_000_000_000..;

tlv_stream!(
ExperimentalInvoiceTlvStream, ExperimentalInvoiceTlvStreamRef, EXPERIMENTAL_INVOICE_TYPES, {}
);

pub(super) type BlindedPathIter<'a> = core::iter::Map<
core::slice::Iter<'a, BlindedPaymentPath>,
for<'r> fn(&'r BlindedPaymentPath) -> &'r BlindedPath,
Expand All @@ -1312,7 +1332,7 @@ impl_writeable!(FallbackAddress, { version, program });

type FullInvoiceTlvStream =(
PayerTlvStream, OfferTlvStream, InvoiceRequestTlvStream, InvoiceTlvStream, SignatureTlvStream,
ExperimentalOfferTlvStream, ExperimentalInvoiceRequestTlvStream,
ExperimentalOfferTlvStream, ExperimentalInvoiceRequestTlvStream, ExperimentalInvoiceTlvStream,
);

type FullInvoiceTlvStreamRef<'a> = (
Expand All @@ -1323,6 +1343,7 @@ type FullInvoiceTlvStreamRef<'a> = (
SignatureTlvStreamRef<'a>,
ExperimentalOfferTlvStreamRef,
ExperimentalInvoiceRequestTlvStreamRef,
ExperimentalInvoiceTlvStreamRef,
);

impl CursorReadable for FullInvoiceTlvStream {
Expand All @@ -1334,19 +1355,20 @@ impl CursorReadable for FullInvoiceTlvStream {
let signature = CursorReadable::read(r)?;
let experimental_offer = CursorReadable::read(r)?;
let experimental_invoice_request = CursorReadable::read(r)?;
let experimental_invoice = CursorReadable::read(r)?;

Ok(
(
payer, offer, invoice_request, invoice, signature, experimental_offer,
experimental_invoice_request,
experimental_invoice_request, experimental_invoice,
)
)
}
}

type PartialInvoiceTlvStream = (
PayerTlvStream, OfferTlvStream, InvoiceRequestTlvStream, InvoiceTlvStream,
ExperimentalOfferTlvStream, ExperimentalInvoiceRequestTlvStream,
ExperimentalOfferTlvStream, ExperimentalInvoiceRequestTlvStream, ExperimentalInvoiceTlvStream,
);

type PartialInvoiceTlvStreamRef<'a> = (
Expand All @@ -1356,6 +1378,7 @@ type PartialInvoiceTlvStreamRef<'a> = (
InvoiceTlvStreamRef<'a>,
ExperimentalOfferTlvStreamRef,
ExperimentalInvoiceRequestTlvStreamRef,
ExperimentalInvoiceTlvStreamRef,
);

impl CursorReadable for PartialInvoiceTlvStream {
Expand All @@ -1366,11 +1389,12 @@ impl CursorReadable for PartialInvoiceTlvStream {
let invoice = CursorReadable::read(r)?;
let experimental_offer = CursorReadable::read(r)?;
let experimental_invoice_request = CursorReadable::read(r)?;
let experimental_invoice = CursorReadable::read(r)?;

Ok(
(
payer, offer, invoice_request, invoice, experimental_offer,
experimental_invoice_request,
experimental_invoice_request, experimental_invoice,
)
)
}
Expand All @@ -1386,11 +1410,13 @@ impl TryFrom<ParsedMessage<FullInvoiceTlvStream>> for Bolt12Invoice {
SignatureTlvStream { signature },
experimental_offer_tlv_stream,
experimental_invoice_request_tlv_stream,
experimental_invoice_tlv_stream,
) = tlv_stream;
let contents = InvoiceContents::try_from(
(
payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream,
experimental_offer_tlv_stream, experimental_invoice_request_tlv_stream,
experimental_invoice_tlv_stream,
)
)?;

Expand Down Expand Up @@ -1419,6 +1445,7 @@ impl TryFrom<PartialInvoiceTlvStream> for InvoiceContents {
},
experimental_offer_tlv_stream,
experimental_invoice_request_tlv_stream,
ExperimentalInvoiceTlvStream {},
) = tlv_stream;

if message_paths.is_some() { return Err(Bolt12SemanticError::UnexpectedPaths) }
Expand Down Expand Up @@ -1515,7 +1542,7 @@ pub(super) fn check_invoice_signing_pubkey(

#[cfg(test)]
mod tests {
use super::{Bolt12Invoice, DEFAULT_RELATIVE_EXPIRY, FallbackAddress, FullInvoiceTlvStreamRef, INVOICE_TYPES, InvoiceTlvStreamRef, SIGNATURE_TAG, UnsignedBolt12Invoice};
use super::{Bolt12Invoice, DEFAULT_RELATIVE_EXPIRY, ExperimentalInvoiceTlvStreamRef, FallbackAddress, FullInvoiceTlvStreamRef, INVOICE_TYPES, InvoiceTlvStreamRef, SIGNATURE_TAG, UnsignedBolt12Invoice};

use bitcoin::{CompressedPublicKey, WitnessProgram, WitnessVersion};
use bitcoin::constants::ChainHash;
Expand Down Expand Up @@ -1711,6 +1738,7 @@ mod tests {
ExperimentalInvoiceRequestTlvStreamRef {
experimental_bar: None,
},
ExperimentalInvoiceTlvStreamRef {},
),
);

Expand Down Expand Up @@ -1810,6 +1838,7 @@ mod tests {
ExperimentalInvoiceRequestTlvStreamRef {
experimental_bar: None,
},
ExperimentalInvoiceTlvStreamRef {},
),
);

Expand Down Expand Up @@ -2006,7 +2035,7 @@ mod tests {
.relative_expiry(one_hour.as_secs() as u32)
.build().unwrap()
.sign(recipient_sign).unwrap();
let (_, _, _, tlv_stream, _, _, _) = invoice.as_tlv_stream();
let (_, _, _, tlv_stream, _, _, _, _) = invoice.as_tlv_stream();
#[cfg(feature = "std")]
assert!(!invoice.is_expired());
assert_eq!(invoice.relative_expiry(), one_hour);
Expand All @@ -2022,7 +2051,7 @@ mod tests {
.relative_expiry(one_hour.as_secs() as u32 - 1)
.build().unwrap()
.sign(recipient_sign).unwrap();
let (_, _, _, tlv_stream, _, _, _) = invoice.as_tlv_stream();
let (_, _, _, tlv_stream, _, _, _, _) = invoice.as_tlv_stream();
#[cfg(feature = "std")]
assert!(invoice.is_expired());
assert_eq!(invoice.relative_expiry(), one_hour - Duration::from_secs(1));
Expand All @@ -2041,7 +2070,7 @@ mod tests {
.respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap()
.build().unwrap()
.sign(recipient_sign).unwrap();
let (_, _, _, tlv_stream, _, _, _) = invoice.as_tlv_stream();
let (_, _, _, tlv_stream, _, _, _, _) = invoice.as_tlv_stream();
assert_eq!(invoice.amount_msats(), 1001);
assert_eq!(tlv_stream.amount, Some(1001));
}
Expand All @@ -2059,7 +2088,7 @@ mod tests {
.respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap()
.build().unwrap()
.sign(recipient_sign).unwrap();
let (_, _, _, tlv_stream, _, _, _) = invoice.as_tlv_stream();
let (_, _, _, tlv_stream, _, _, _, _) = invoice.as_tlv_stream();
assert_eq!(invoice.amount_msats(), 2000);
assert_eq!(tlv_stream.amount, Some(2000));

Expand Down Expand Up @@ -2097,7 +2126,7 @@ mod tests {
.fallback_v1_p2tr_tweaked(&tweaked_pubkey)
.build().unwrap()
.sign(recipient_sign).unwrap();
let (_, _, _, tlv_stream, _, _, _) = invoice.as_tlv_stream();
let (_, _, _, tlv_stream, _, _, _, _) = invoice.as_tlv_stream();
assert_eq!(
invoice.fallbacks(),
vec![
Expand Down Expand Up @@ -2140,7 +2169,7 @@ mod tests {
.allow_mpp()
.build().unwrap()
.sign(recipient_sign).unwrap();
let (_, _, _, tlv_stream, _, _, _) = invoice.as_tlv_stream();
let (_, _, _, tlv_stream, _, _, _, _) = invoice.as_tlv_stream();
assert_eq!(invoice.invoice_features(), &features);
assert_eq!(tlv_stream.features, Some(&features));
}
Expand Down
12 changes: 8 additions & 4 deletions lightning/src/offers/invoice_request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1539,7 +1539,7 @@ mod tests {
let (
payer_tlv_stream, offer_tlv_stream, mut invoice_request_tlv_stream,
mut invoice_tlv_stream, mut signature_tlv_stream, experimental_offer_tlv_stream,
experimental_invoice_request_tlv_stream,
experimental_invoice_request_tlv_stream, experimental_invoice_tlv_stream,
) = invoice.as_tlv_stream();
invoice_request_tlv_stream.amount = Some(2000);
invoice_tlv_stream.amount = Some(2000);
Expand All @@ -1548,6 +1548,7 @@ mod tests {
(payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream);
let experimental_tlv_stream = (
experimental_offer_tlv_stream, experimental_invoice_request_tlv_stream,
experimental_invoice_tlv_stream,
);
let mut bytes = Vec::new();
(&tlv_stream, &experimental_tlv_stream).write(&mut bytes).unwrap();
Expand All @@ -1568,7 +1569,7 @@ mod tests {
let (
mut payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream,
mut signature_tlv_stream, experimental_offer_tlv_stream,
experimental_invoice_request_tlv_stream,
experimental_invoice_request_tlv_stream, experimental_invoice_tlv_stream,
) = invoice.as_tlv_stream();
let metadata = payer_tlv_stream.metadata.unwrap().iter().copied().rev().collect();
payer_tlv_stream.metadata = Some(&metadata);
Expand All @@ -1577,6 +1578,7 @@ mod tests {
(payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream);
let experimental_tlv_stream = (
experimental_offer_tlv_stream, experimental_invoice_request_tlv_stream,
experimental_invoice_tlv_stream,
);
let mut bytes = Vec::new();
(&tlv_stream, &experimental_tlv_stream).write(&mut bytes).unwrap();
Expand Down Expand Up @@ -1626,7 +1628,7 @@ mod tests {
let (
payer_tlv_stream, offer_tlv_stream, mut invoice_request_tlv_stream,
mut invoice_tlv_stream, mut signature_tlv_stream, experimental_offer_tlv_stream,
experimental_invoice_request_tlv_stream,
experimental_invoice_request_tlv_stream, experimental_invoice_tlv_stream,
) = invoice.as_tlv_stream();
invoice_request_tlv_stream.amount = Some(2000);
invoice_tlv_stream.amount = Some(2000);
Expand All @@ -1635,6 +1637,7 @@ mod tests {
(payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream);
let experimental_tlv_stream = (
experimental_offer_tlv_stream, experimental_invoice_request_tlv_stream,
experimental_invoice_tlv_stream,
);
let mut bytes = Vec::new();
(&tlv_stream, &experimental_tlv_stream).write(&mut bytes).unwrap();
Expand All @@ -1657,7 +1660,7 @@ mod tests {
let (
payer_tlv_stream, offer_tlv_stream, mut invoice_request_tlv_stream, invoice_tlv_stream,
mut signature_tlv_stream, experimental_offer_tlv_stream,
experimental_invoice_request_tlv_stream,
experimental_invoice_request_tlv_stream, experimental_invoice_tlv_stream,
) = invoice.as_tlv_stream();
let payer_id = pubkey(1);
invoice_request_tlv_stream.payer_id = Some(&payer_id);
Expand All @@ -1666,6 +1669,7 @@ mod tests {
(payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream);
let experimental_tlv_stream = (
experimental_offer_tlv_stream, experimental_invoice_request_tlv_stream,
experimental_invoice_tlv_stream,
);
let mut bytes = Vec::new();
(&tlv_stream, &experimental_tlv_stream).write(&mut bytes).unwrap();
Expand Down
Loading

0 comments on commit 575433d

Please sign in to comment.