]> git.bitcoin.ninja Git - rust-lightning/commitdiff
Separate bytes for experimental TLVs
authorJeffrey Czyz <jkczyz@gmail.com>
Fri, 9 Aug 2024 23:36:24 +0000 (18:36 -0500)
committerJeffrey Czyz <jkczyz@gmail.com>
Tue, 5 Nov 2024 00:00:15 +0000 (18:00 -0600)
When constructing UnsignedInvoiceRequest or UnsignedBolt12Invoice, use a
separate field for experimental TLV bytes. This allows for properly
inserting the signature TLVs before the experimental TLVs when signing.

lightning/src/offers/invoice.rs
lightning/src/offers/invoice_request.rs
lightning/src/offers/merkle.rs
lightning/src/offers/offer.rs
lightning/src/offers/static_invoice.rs

index 8fd9ab980fa95e4d64388e2ce40ff01ea01fff61..48d0ea1ba76e8849430fd1d2aa7ea66e5402c40f 100644 (file)
@@ -121,10 +121,10 @@ use crate::ln::msgs::DecodeError;
 use crate::offers::invoice_macros::{invoice_accessors_common, invoice_builder_methods_common};
 #[cfg(test)]
 use crate::offers::invoice_macros::invoice_builder_methods_test;
-use crate::offers::invoice_request::{INVOICE_REQUEST_PAYER_ID_TYPE, INVOICE_REQUEST_TYPES, IV_BYTES as INVOICE_REQUEST_IV_BYTES, InvoiceRequest, InvoiceRequestContents, InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef};
-use crate::offers::merkle::{SignError, SignFn, SignatureTlvStream, SignatureTlvStreamRef, TaggedHash, TlvStream, WithoutSignatures, self};
+use crate::offers::invoice_request::{EXPERIMENTAL_INVOICE_REQUEST_TYPES, INVOICE_REQUEST_PAYER_ID_TYPE, INVOICE_REQUEST_TYPES, IV_BYTES as INVOICE_REQUEST_IV_BYTES, InvoiceRequest, InvoiceRequestContents, InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef};
+use crate::offers::merkle::{SignError, SignFn, SignatureTlvStream, SignatureTlvStreamRef, TaggedHash, TlvStream, self, SIGNATURE_TLV_RECORD_SIZE};
 use crate::offers::nonce::Nonce;
-use crate::offers::offer::{Amount, OFFER_TYPES, OfferTlvStream, OfferTlvStreamRef, Quantity};
+use crate::offers::offer::{Amount, EXPERIMENTAL_OFFER_TYPES, OFFER_TYPES, OfferTlvStream, OfferTlvStreamRef, Quantity};
 use crate::offers::parse::{Bolt12ParseError, Bolt12SemanticError, ParsedMessage};
 use crate::offers::payer::{PAYER_METADATA_TYPE, PayerTlvStream, PayerTlvStreamRef};
 use crate::offers::refund::{IV_BYTES_WITH_METADATA as REFUND_IV_BYTES_WITH_METADATA, IV_BYTES_WITHOUT_METADATA as REFUND_IV_BYTES_WITHOUT_METADATA, Refund, RefundContents};
@@ -461,6 +461,7 @@ for InvoiceBuilder<'a, DerivedSigningPubkey> {
 #[derive(Clone)]
 pub struct UnsignedBolt12Invoice {
        bytes: Vec<u8>,
+       experimental_bytes: Vec<u8>,
        contents: InvoiceContents,
        tagged_hash: TaggedHash,
 }
@@ -491,19 +492,57 @@ where
 
 impl UnsignedBolt12Invoice {
        fn new(invreq_bytes: &[u8], contents: InvoiceContents) -> Self {
+               // TLV record ranges applicable to invreq_bytes.
+               const NON_EXPERIMENTAL_TYPES: core::ops::Range<u64> = 0..INVOICE_REQUEST_TYPES.end;
+               const EXPERIMENTAL_TYPES: core::ops::Range<u64> =
+                       EXPERIMENTAL_OFFER_TYPES.start..EXPERIMENTAL_INVOICE_REQUEST_TYPES.end;
+
+               let (_, _, _, invoice_tlv_stream) = contents.as_tlv_stream();
+
+               // Allocate enough space for the invoice, which will include:
+               // - all TLV records from `invreq_bytes` except signatures,
+               // - all invoice-specific TLV records, and
+               // - a signature TLV record once the invoice is signed.
+               //
+               // This assumes both the invoice request and the invoice will each only have one signature
+               // using SIGNATURE_TYPES.start as the TLV record. Thus, it is accounted for by invreq_bytes.
+               let mut bytes = Vec::with_capacity(
+                       invreq_bytes.len()
+                               + invoice_tlv_stream.serialized_length()
+                               + if contents.is_for_offer() { 0 } else { SIGNATURE_TLV_RECORD_SIZE }
+               );
+
                // Use the invoice_request bytes instead of the invoice_request TLV stream as the latter may
                // have contained unknown TLV records, which are not stored in `InvoiceRequestContents` or
                // `RefundContents`.
-               let (_, _, _, invoice_tlv_stream) = contents.as_tlv_stream();
-               let invoice_request_bytes = WithoutSignatures(invreq_bytes);
-               let unsigned_tlv_stream = (invoice_request_bytes, invoice_tlv_stream);
+               for record in TlvStream::new(invreq_bytes).range(NON_EXPERIMENTAL_TYPES) {
+                       record.write(&mut bytes).unwrap();
+               }
 
-               let mut bytes = Vec::new();
-               unsigned_tlv_stream.write(&mut bytes).unwrap();
+               let remaining_bytes = &invreq_bytes[bytes.len()..];
 
-               let tagged_hash = TaggedHash::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &bytes);
+               invoice_tlv_stream.write(&mut bytes).unwrap();
+
+               let mut experimental_tlv_stream = TlvStream::new(remaining_bytes)
+                       .range(EXPERIMENTAL_TYPES)
+                       .peekable();
+               let mut experimental_bytes = Vec::with_capacity(
+                       remaining_bytes.len()
+                               - experimental_tlv_stream
+                                       .peek()
+                                       .map_or(remaining_bytes.len(), |first_record| first_record.start)
+               );
+
+               for record in experimental_tlv_stream {
+                       record.write(&mut experimental_bytes).unwrap();
+               }
 
-               Self { bytes, contents, tagged_hash }
+               debug_assert_eq!(experimental_bytes.len(), experimental_bytes.capacity());
+
+               let tlv_stream = TlvStream::new(&bytes).chain(TlvStream::new(&experimental_bytes));
+               let tagged_hash = TaggedHash::from_tlv_stream(SIGNATURE_TAG, tlv_stream);
+
+               Self { bytes, experimental_bytes, contents, tagged_hash }
        }
 
        /// Returns the [`TaggedHash`] of the invoice to sign.
@@ -528,6 +567,17 @@ macro_rules! unsigned_invoice_sign_method { ($self: ident, $self_type: ty $(, $s
                };
                signature_tlv_stream.write(&mut $self.bytes).unwrap();
 
+               // Append the experimental bytes after the signature.
+               debug_assert_eq!(
+                       // The two-byte overallocation results from SIGNATURE_TLV_RECORD_SIZE accommodating TLV
+                       // records with types >= 253.
+                       $self.bytes.len()
+                               + $self.experimental_bytes.len()
+                               + if $self.contents.is_for_offer() { 0 } else { 2 },
+                       $self.bytes.capacity(),
+               );
+               $self.bytes.extend_from_slice(&$self.experimental_bytes);
+
                Ok(Bolt12Invoice {
                        #[cfg(not(c_bindings))]
                        bytes: $self.bytes,
@@ -882,6 +932,13 @@ impl Hash for Bolt12Invoice {
 }
 
 impl InvoiceContents {
+       fn is_for_offer(&self) -> bool {
+               match self {
+                       InvoiceContents::ForOffer { .. } => true,
+                       InvoiceContents::ForRefund { .. } => false,
+               }
+       }
+
        /// Whether the original offer or refund has expired.
        #[cfg(feature = "std")]
        fn is_offer_or_refund_expired(&self) -> bool {
@@ -1211,7 +1268,7 @@ impl TryFrom<Vec<u8>> for UnsignedBolt12Invoice {
 
        fn try_from(bytes: Vec<u8>) -> Result<Self, Self::Error> {
                let invoice = ParsedMessage::<PartialInvoiceTlvStream>::try_from(bytes)?;
-               let ParsedMessage { bytes, tlv_stream } = invoice;
+               let ParsedMessage { mut bytes, tlv_stream } = invoice;
                let (
                        payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream,
                ) = tlv_stream;
@@ -1221,7 +1278,13 @@ impl TryFrom<Vec<u8>> for UnsignedBolt12Invoice {
 
                let tagged_hash = TaggedHash::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &bytes);
 
-               Ok(UnsignedBolt12Invoice { bytes, contents, tagged_hash })
+               let offset = TlvStream::new(&bytes)
+                       .range(0..INVOICE_TYPES.end)
+                       .last()
+                       .map_or(0, |last_record| last_record.end);
+               let experimental_bytes = bytes.split_off(offset);
+
+               Ok(UnsignedBolt12Invoice { bytes, experimental_bytes, contents, tagged_hash })
        }
 }
 
@@ -2512,10 +2575,15 @@ mod tests {
                        .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap()
                        .build().unwrap();
 
-               BigSize(UNKNOWN_ODD_TYPE).write(&mut unsigned_invoice.bytes).unwrap();
-               BigSize(32).write(&mut unsigned_invoice.bytes).unwrap();
-               [42u8; 32].write(&mut unsigned_invoice.bytes).unwrap();
+               let mut unknown_bytes = Vec::new();
+               BigSize(UNKNOWN_ODD_TYPE).write(&mut unknown_bytes).unwrap();
+               BigSize(32).write(&mut unknown_bytes).unwrap();
+               [42u8; 32].write(&mut unknown_bytes).unwrap();
 
+               unsigned_invoice.bytes.reserve_exact(
+                       unsigned_invoice.bytes.capacity() - unsigned_invoice.bytes.len() + unknown_bytes.len(),
+               );
+               unsigned_invoice.bytes.extend_from_slice(&unknown_bytes);
                unsigned_invoice.tagged_hash =
                        TaggedHash::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &unsigned_invoice.bytes);
 
@@ -2545,10 +2613,15 @@ mod tests {
                        .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap()
                        .build().unwrap();
 
-               BigSize(UNKNOWN_EVEN_TYPE).write(&mut unsigned_invoice.bytes).unwrap();
-               BigSize(32).write(&mut unsigned_invoice.bytes).unwrap();
-               [42u8; 32].write(&mut unsigned_invoice.bytes).unwrap();
+               let mut unknown_bytes = Vec::new();
+               BigSize(UNKNOWN_EVEN_TYPE).write(&mut unknown_bytes).unwrap();
+               BigSize(32).write(&mut unknown_bytes).unwrap();
+               [42u8; 32].write(&mut unknown_bytes).unwrap();
 
+               unsigned_invoice.bytes.reserve_exact(
+                       unsigned_invoice.bytes.capacity() - unsigned_invoice.bytes.len() + unknown_bytes.len(),
+               );
+               unsigned_invoice.bytes.extend_from_slice(&unknown_bytes);
                unsigned_invoice.tagged_hash =
                        TaggedHash::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &unsigned_invoice.bytes);
 
index 4d5c37bbaa993d0419f884d5a7d5aed0e22bdf9c..5ec4ab8a7a297a352ca48d4258be408a6b2c8d1f 100644 (file)
@@ -69,9 +69,9 @@ use crate::ln::channelmanager::PaymentId;
 use crate::ln::features::InvoiceRequestFeatures;
 use crate::ln::inbound_payment::{ExpandedKey, IV_LEN};
 use crate::ln::msgs::DecodeError;
-use crate::offers::merkle::{SignError, SignFn, SignatureTlvStream, SignatureTlvStreamRef, TaggedHash, self};
+use crate::offers::merkle::{SignError, SignFn, SignatureTlvStream, SignatureTlvStreamRef, TaggedHash, TlvStream, self, SIGNATURE_TLV_RECORD_SIZE};
 use crate::offers::nonce::Nonce;
-use crate::offers::offer::{Offer, OfferContents, OfferId, OfferTlvStream, OfferTlvStreamRef};
+use crate::offers::offer::{EXPERIMENTAL_OFFER_TYPES, OFFER_TYPES, Offer, OfferContents, OfferId, OfferTlvStream, OfferTlvStreamRef};
 use crate::offers::parse::{Bolt12ParseError, ParsedMessage, Bolt12SemanticError};
 use crate::offers::payer::{PayerContents, PayerTlvStream, PayerTlvStreamRef};
 use crate::offers::signer::{Metadata, MetadataMaterial};
@@ -488,6 +488,7 @@ for InvoiceRequestBuilder<'a, 'b, DerivedPayerSigningPubkey, secp256k1::All> {
 #[derive(Clone)]
 pub struct UnsignedInvoiceRequest {
        bytes: Vec<u8>,
+       experimental_bytes: Vec<u8>,
        contents: InvoiceRequestContents,
        tagged_hash: TaggedHash,
 }
@@ -520,17 +521,51 @@ impl UnsignedInvoiceRequest {
        fn new(offer: &Offer, contents: InvoiceRequestContents) -> Self {
                // Use the offer bytes instead of the offer TLV stream as the offer may have contained
                // unknown TLV records, which are not stored in `OfferContents`.
-               let (payer_tlv_stream, _offer_tlv_stream, invoice_request_tlv_stream) =
-                       contents.as_tlv_stream();
-               let offer_bytes = WithoutLength(&offer.bytes);
-               let unsigned_tlv_stream = (payer_tlv_stream, offer_bytes, invoice_request_tlv_stream);
+               let (
+                       payer_tlv_stream, _offer_tlv_stream, invoice_request_tlv_stream,
+               ) = contents.as_tlv_stream();
+
+               // Allocate enough space for the invoice_request, which will include:
+               // - all TLV records from `offer.bytes`,
+               // - all invoice_request-specific TLV records, and
+               // - a signature TLV record once the invoice_request is signed.
+               let mut bytes = Vec::with_capacity(
+                       offer.bytes.len()
+                               + payer_tlv_stream.serialized_length()
+                               + invoice_request_tlv_stream.serialized_length()
+                               + SIGNATURE_TLV_RECORD_SIZE
+               );
 
-               let mut bytes = Vec::new();
-               unsigned_tlv_stream.write(&mut bytes).unwrap();
+               payer_tlv_stream.write(&mut bytes).unwrap();
 
-               let tagged_hash = TaggedHash::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &bytes);
+               for record in TlvStream::new(&offer.bytes).range(OFFER_TYPES) {
+                       record.write(&mut bytes).unwrap();
+               }
+
+               let remaining_bytes = &offer.bytes[bytes.len() - payer_tlv_stream.serialized_length()..];
+
+               invoice_request_tlv_stream.write(&mut bytes).unwrap();
+
+               let mut experimental_tlv_stream = TlvStream::new(remaining_bytes)
+                       .range(EXPERIMENTAL_OFFER_TYPES)
+                       .peekable();
+               let mut experimental_bytes = Vec::with_capacity(
+                       remaining_bytes.len()
+                               - experimental_tlv_stream
+                                       .peek()
+                                       .map_or(remaining_bytes.len(), |first_record| first_record.start)
+               );
+
+               for record in experimental_tlv_stream {
+                       record.write(&mut experimental_bytes).unwrap();
+               }
+
+               debug_assert_eq!(experimental_bytes.len(), experimental_bytes.capacity());
+
+               let tlv_stream = TlvStream::new(&bytes).chain(TlvStream::new(&experimental_bytes));
+               let tagged_hash = TaggedHash::from_tlv_stream(SIGNATURE_TAG, tlv_stream);
 
-               Self { bytes, contents, tagged_hash }
+               Self { bytes, experimental_bytes, contents, tagged_hash }
        }
 
        /// Returns the [`TaggedHash`] of the invoice to sign.
@@ -557,6 +592,15 @@ macro_rules! unsigned_invoice_request_sign_method { (
                };
                signature_tlv_stream.write(&mut $self.bytes).unwrap();
 
+               // Append the experimental bytes after the signature.
+               debug_assert_eq!(
+                       // The two-byte overallocation results from SIGNATURE_TLV_RECORD_SIZE accommodating TLV
+                       // records with types >= 253.
+                       $self.bytes.len() + $self.experimental_bytes.len() + 2,
+                       $self.bytes.capacity(),
+               );
+               $self.bytes.extend_from_slice(&$self.experimental_bytes);
+
                Ok(InvoiceRequest {
                        #[cfg(not(c_bindings))]
                        bytes: $self.bytes,
@@ -1072,6 +1116,10 @@ tlv_stream!(InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef<'a>, INVOICE_REQ
        (90, paths: (Vec<BlindedMessagePath>, WithoutLength)),
 });
 
+/// Valid type range for experimental invoice_request TLV records.
+pub(super) const EXPERIMENTAL_INVOICE_REQUEST_TYPES: core::ops::Range<u64> =
+       2_000_000_000..3_000_000_000;
+
 type FullInvoiceRequestTlvStream =
        (PayerTlvStream, OfferTlvStream, InvoiceRequestTlvStream, SignatureTlvStream);
 
@@ -1106,7 +1154,7 @@ impl TryFrom<Vec<u8>> for UnsignedInvoiceRequest {
 
        fn try_from(bytes: Vec<u8>) -> Result<Self, Self::Error> {
                let invoice_request = ParsedMessage::<PartialInvoiceRequestTlvStream>::try_from(bytes)?;
-               let ParsedMessage { bytes, tlv_stream } = invoice_request;
+               let ParsedMessage { mut bytes, tlv_stream } = invoice_request;
                let (
                        payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream,
                ) = tlv_stream;
@@ -1116,7 +1164,13 @@ impl TryFrom<Vec<u8>> for UnsignedInvoiceRequest {
 
                let tagged_hash = TaggedHash::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &bytes);
 
-               Ok(UnsignedInvoiceRequest { bytes, contents, tagged_hash })
+               let offset = TlvStream::new(&bytes)
+                       .range(0..INVOICE_REQUEST_TYPES.end)
+                       .last()
+                       .map_or(0, |last_record| last_record.end);
+               let experimental_bytes = bytes.split_off(offset);
+
+               Ok(UnsignedInvoiceRequest { bytes, experimental_bytes, contents, tagged_hash })
        }
 }
 
@@ -2306,10 +2360,17 @@ mod tests {
                        .request_invoice(vec![1; 32], keys.public_key()).unwrap()
                        .build().unwrap();
 
-               BigSize(UNKNOWN_ODD_TYPE).write(&mut unsigned_invoice_request.bytes).unwrap();
-               BigSize(32).write(&mut unsigned_invoice_request.bytes).unwrap();
-               [42u8; 32].write(&mut unsigned_invoice_request.bytes).unwrap();
+               let mut unknown_bytes = Vec::new();
+               BigSize(UNKNOWN_ODD_TYPE).write(&mut unknown_bytes).unwrap();
+               BigSize(32).write(&mut unknown_bytes).unwrap();
+               [42u8; 32].write(&mut unknown_bytes).unwrap();
 
+               unsigned_invoice_request.bytes.reserve_exact(
+                       unsigned_invoice_request.bytes.capacity()
+                               - unsigned_invoice_request.bytes.len()
+                               + unknown_bytes.len(),
+               );
+               unsigned_invoice_request.bytes.extend_from_slice(&unknown_bytes);
                unsigned_invoice_request.tagged_hash =
                        TaggedHash::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &unsigned_invoice_request.bytes);
 
@@ -2336,10 +2397,17 @@ mod tests {
                        .request_invoice(vec![1; 32], keys.public_key()).unwrap()
                        .build().unwrap();
 
-               BigSize(UNKNOWN_EVEN_TYPE).write(&mut unsigned_invoice_request.bytes).unwrap();
-               BigSize(32).write(&mut unsigned_invoice_request.bytes).unwrap();
-               [42u8; 32].write(&mut unsigned_invoice_request.bytes).unwrap();
+               let mut unknown_bytes = Vec::new();
+               BigSize(UNKNOWN_EVEN_TYPE).write(&mut unknown_bytes).unwrap();
+               BigSize(32).write(&mut unknown_bytes).unwrap();
+               [42u8; 32].write(&mut unknown_bytes).unwrap();
 
+               unsigned_invoice_request.bytes.reserve_exact(
+                       unsigned_invoice_request.bytes.capacity()
+                               - unsigned_invoice_request.bytes.len()
+                               + unknown_bytes.len(),
+               );
+               unsigned_invoice_request.bytes.extend_from_slice(&unknown_bytes);
                unsigned_invoice_request.tagged_hash =
                        TaggedHash::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &unsigned_invoice_request.bytes);
 
index 6cbad62790918e7131e890d4d28634ea1d4050c0..3497881faf99cb6fff9116446f8ce592be5089dc 100644 (file)
@@ -11,6 +11,7 @@
 
 use bitcoin::hashes::{Hash, HashEngine, sha256};
 use bitcoin::secp256k1::{Message, PublicKey, Secp256k1, self};
+use bitcoin::secp256k1::constants::SCHNORR_SIGNATURE_SIZE;
 use bitcoin::secp256k1::schnorr::Signature;
 use crate::io;
 use crate::util::ser::{BigSize, Readable, Writeable, Writer};
@@ -19,12 +20,16 @@ use crate::util::ser::{BigSize, Readable, Writeable, Writer};
 use crate::prelude::*;
 
 /// Valid type range for signature TLV records.
-const SIGNATURE_TYPES: core::ops::RangeInclusive<u64> = 240..=1000;
+pub(super) const SIGNATURE_TYPES: core::ops::RangeInclusive<u64> = 240..=1000;
 
 tlv_stream!(SignatureTlvStream, SignatureTlvStreamRef<'a>, SIGNATURE_TYPES, {
        (240, signature: Signature),
 });
 
+/// Size of a TLV record in `SIGNATURE_TYPES` when the type is 1000. TLV types are encoded using
+/// BigSize, so a TLV record with type 240 will use two less bytes.
+pub(super) const SIGNATURE_TLV_RECORD_SIZE: usize = 3 + 1 + SCHNORR_SIGNATURE_SIZE;
+
 /// A hash for use in a specific context by tweaking with a context-dependent tag as per [BIP 340]
 /// and computed over the merkle root of a TLV stream to sign as defined in [BOLT 12].
 ///
@@ -164,7 +169,7 @@ fn root_hash<'a, I: core::iter::Iterator<Item = TlvRecord<'a>>>(tlv_stream: I) -
        let branch_tag = tagged_hash_engine(sha256::Hash::hash("LnBranch".as_bytes()));
 
        let mut leaves = Vec::new();
-       for record in TlvStream::skip_signatures(tlv_stream) {
+       for record in tlv_stream.filter(|record| !SIGNATURE_TYPES.contains(&record.r#type)) {
                leaves.push(tagged_hash_from_engine(leaf_tag.clone(), &record.record_bytes));
                leaves.push(tagged_hash_from_engine(nonce_tag.clone(), &record.type_bytes));
        }
@@ -240,12 +245,6 @@ impl<'a> TlvStream<'a> {
                self.skip_while(move |record| !types.contains(&record.r#type))
                        .take_while(move |record| take_range.contains(&record.r#type))
        }
-
-       fn skip_signatures(
-               tlv_stream: impl core::iter::Iterator<Item = TlvRecord<'a>>
-       ) -> impl core::iter::Iterator<Item = TlvRecord<'a>> {
-               tlv_stream.filter(|record| !SIGNATURE_TYPES.contains(&record.r#type))
-       }
 }
 
 /// A slice into a [`TlvStream`] for a record.
@@ -254,6 +253,8 @@ pub(super) struct TlvRecord<'a> {
        type_bytes: &'a [u8],
        // The entire TLV record.
        pub(super) record_bytes: &'a [u8],
+       pub(super) start: usize,
+       pub(super) end: usize,
 }
 
 impl<'a> Iterator for TlvStream<'a> {
@@ -276,32 +277,25 @@ impl<'a> Iterator for TlvStream<'a> {
 
                        self.data.set_position(end);
 
-                       Some(TlvRecord { r#type, type_bytes, record_bytes })
+                       Some(TlvRecord {
+                               r#type, type_bytes, record_bytes, start: start as usize, end: end as usize,
+                       })
                } else {
                        None
                }
        }
 }
 
-/// Encoding for a pre-serialized TLV stream that excludes any signature TLV records.
-///
-/// Panics if the wrapped bytes are not a well-formed TLV stream.
-pub(super) struct WithoutSignatures<'a>(pub &'a [u8]);
-
-impl<'a> Writeable for WithoutSignatures<'a> {
+impl<'a> Writeable for TlvRecord<'a> {
        #[inline]
        fn write<W: Writer>(&self, writer: &mut W) -> Result<(), io::Error> {
-               let tlv_stream = TlvStream::new(self.0);
-               for record in TlvStream::skip_signatures(tlv_stream) {
-                       writer.write_all(record.record_bytes)?;
-               }
-               Ok(())
+               writer.write_all(self.record_bytes)
        }
 }
 
 #[cfg(test)]
 mod tests {
-       use super::{SIGNATURE_TYPES, TlvStream, WithoutSignatures};
+       use super::{SIGNATURE_TYPES, TlvStream};
 
        use bitcoin::hashes::{Hash, sha256};
        use bitcoin::hex::FromHex;
@@ -411,7 +405,11 @@ mod tests {
                        .unwrap();
 
                let mut bytes_without_signature = Vec::new();
-               WithoutSignatures(&invoice_request.bytes).write(&mut bytes_without_signature).unwrap();
+               let tlv_stream_without_signatures = TlvStream::new(&invoice_request.bytes)
+                       .filter(|record| !SIGNATURE_TYPES.contains(&record.r#type));
+               for record in tlv_stream_without_signatures {
+                       record.write(&mut bytes_without_signature).unwrap();
+               }
 
                assert_ne!(bytes_without_signature, invoice_request.bytes);
                assert_eq!(
index 4caa3757a349df51f667e1a5831e724142e8031f..675395841cda5dabc316b7bf9377b10ee74422fd 100644 (file)
@@ -1091,6 +1091,9 @@ tlv_stream!(OfferTlvStream, OfferTlvStreamRef<'a>, OFFER_TYPES, {
        (OFFER_ISSUER_ID_TYPE, issuer_id: PublicKey),
 });
 
+/// Valid type range for experimental offer TLV records.
+pub(super) const EXPERIMENTAL_OFFER_TYPES: core::ops::Range<u64> = 1_000_000_000..2_000_000_000;
+
 impl Bech32Encode for Offer {
        const BECH32_HRP: &'static str = "lno";
 }
index dd3fb8b150b894a377f2fc56aaaab363533d15d5..07f2b9d31524eab0d7a31004184e6333f3b69edf 100644 (file)
@@ -23,10 +23,12 @@ use crate::offers::invoice_macros::{invoice_accessors_common, invoice_builder_me
 use crate::offers::invoice_request::InvoiceRequest;
 use crate::offers::merkle::{
        self, SignError, SignFn, SignatureTlvStream, SignatureTlvStreamRef, TaggedHash, TlvStream,
+       SIGNATURE_TLV_RECORD_SIZE,
 };
 use crate::offers::nonce::Nonce;
 use crate::offers::offer::{
-       Amount, Offer, OfferContents, OfferTlvStream, OfferTlvStreamRef, Quantity, OFFER_TYPES,
+       Amount, Offer, OfferContents, OfferTlvStream, OfferTlvStreamRef, Quantity,
+       EXPERIMENTAL_OFFER_TYPES, OFFER_TYPES,
 };
 use crate::offers::parse::{Bolt12ParseError, Bolt12SemanticError, ParsedMessage};
 use crate::util::ser::{CursorReadable, Iterable, WithoutLength, Writeable, Writer};
@@ -170,6 +172,7 @@ impl<'a> StaticInvoiceBuilder<'a> {
 /// A semantically valid [`StaticInvoice`] that hasn't been signed.
 pub struct UnsignedStaticInvoice {
        bytes: Vec<u8>,
+       experimental_bytes: Vec<u8>,
        contents: InvoiceContents,
        tagged_hash: TaggedHash,
 }
@@ -276,15 +279,44 @@ macro_rules! invoice_accessors_signing_pubkey {
 impl UnsignedStaticInvoice {
        fn new(offer_bytes: &Vec<u8>, contents: InvoiceContents) -> Self {
                let (_, invoice_tlv_stream) = contents.as_tlv_stream();
-               let offer_bytes = WithoutLength(offer_bytes);
-               let unsigned_tlv_stream = (offer_bytes, invoice_tlv_stream);
 
-               let mut bytes = Vec::new();
-               unsigned_tlv_stream.write(&mut bytes).unwrap();
+               // Allocate enough space for the invoice, which will include:
+               // - all TLV records from `offer_bytes`,
+               // - all invoice-specific TLV records, and
+               // - a signature TLV record once the invoice is signed.
+               let mut bytes = Vec::with_capacity(
+                       offer_bytes.len() + invoice_tlv_stream.serialized_length() + SIGNATURE_TLV_RECORD_SIZE,
+               );
 
-               let tagged_hash = TaggedHash::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &bytes);
+               // Use the offer bytes instead of the offer TLV stream as the latter may have contained
+               // unknown TLV records, which are not stored in `InvoiceContents`.
+               for record in TlvStream::new(offer_bytes).range(OFFER_TYPES) {
+                       record.write(&mut bytes).unwrap();
+               }
+
+               let remaining_bytes = &offer_bytes[bytes.len()..];
 
-               Self { contents, tagged_hash, bytes }
+               invoice_tlv_stream.write(&mut bytes).unwrap();
+
+               let mut experimental_tlv_stream =
+                       TlvStream::new(remaining_bytes).range(EXPERIMENTAL_OFFER_TYPES).peekable();
+               let mut experimental_bytes = Vec::with_capacity(
+                       remaining_bytes.len()
+                               - experimental_tlv_stream
+                                       .peek()
+                                       .map_or(remaining_bytes.len(), |first_record| first_record.start),
+               );
+
+               for record in experimental_tlv_stream {
+                       record.write(&mut experimental_bytes).unwrap();
+               }
+
+               debug_assert_eq!(experimental_bytes.len(), experimental_bytes.capacity());
+
+               let tlv_stream = TlvStream::new(&bytes).chain(TlvStream::new(&experimental_bytes));
+               let tagged_hash = TaggedHash::from_tlv_stream(SIGNATURE_TAG, tlv_stream);
+
+               Self { bytes, experimental_bytes, contents, tagged_hash }
        }
 
        /// Signs the [`TaggedHash`] of the invoice using the given function.
@@ -298,6 +330,15 @@ impl UnsignedStaticInvoice {
                let signature_tlv_stream = SignatureTlvStreamRef { signature: Some(&signature) };
                signature_tlv_stream.write(&mut self.bytes).unwrap();
 
+               // Append the experimental bytes after the signature.
+               debug_assert_eq!(
+                       // The two-byte overallocation results from SIGNATURE_TLV_RECORD_SIZE accommodating TLV
+                       // records with types >= 253.
+                       self.bytes.len() + self.experimental_bytes.len() + 2,
+                       self.bytes.capacity(),
+               );
+               self.bytes.extend_from_slice(&self.experimental_bytes);
+
                Ok(StaticInvoice { bytes: self.bytes, contents: self.contents, signature })
        }
 
@@ -1222,10 +1263,15 @@ mod tests {
                .build()
                .unwrap();
 
-               BigSize(UNKNOWN_ODD_TYPE).write(&mut unsigned_invoice.bytes).unwrap();
-               BigSize(32).write(&mut unsigned_invoice.bytes).unwrap();
-               [42u8; 32].write(&mut unsigned_invoice.bytes).unwrap();
+               let mut unknown_bytes = Vec::new();
+               BigSize(UNKNOWN_ODD_TYPE).write(&mut unknown_bytes).unwrap();
+               BigSize(32).write(&mut unknown_bytes).unwrap();
+               [42u8; 32].write(&mut unknown_bytes).unwrap();
 
+               unsigned_invoice.bytes.reserve_exact(
+                       unsigned_invoice.bytes.capacity() - unsigned_invoice.bytes.len() + unknown_bytes.len(),
+               );
+               unsigned_invoice.bytes.extend_from_slice(&unknown_bytes);
                unsigned_invoice.tagged_hash =
                        TaggedHash::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &unsigned_invoice.bytes);
 
@@ -1259,10 +1305,15 @@ mod tests {
                .build()
                .unwrap();
 
-               BigSize(UNKNOWN_EVEN_TYPE).write(&mut unsigned_invoice.bytes).unwrap();
-               BigSize(32).write(&mut unsigned_invoice.bytes).unwrap();
-               [42u8; 32].write(&mut unsigned_invoice.bytes).unwrap();
+               let mut unknown_bytes = Vec::new();
+               BigSize(UNKNOWN_EVEN_TYPE).write(&mut unknown_bytes).unwrap();
+               BigSize(32).write(&mut unknown_bytes).unwrap();
+               [42u8; 32].write(&mut unknown_bytes).unwrap();
 
+               unsigned_invoice.bytes.reserve_exact(
+                       unsigned_invoice.bytes.capacity() - unsigned_invoice.bytes.len() + unknown_bytes.len(),
+               );
+               unsigned_invoice.bytes.extend_from_slice(&unknown_bytes);
                unsigned_invoice.tagged_hash =
                        TaggedHash::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &unsigned_invoice.bytes);