Skip to content

Commit cbbe51f

Browse files
committed
Separate bytes for experimental TLVs
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.
1 parent e23f89c commit cbbe51f

File tree

5 files changed

+186
-53
lines changed

5 files changed

+186
-53
lines changed

lightning/src/offers/invoice.rs

Lines changed: 65 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -121,10 +121,10 @@ use crate::ln::msgs::DecodeError;
121121
use crate::offers::invoice_macros::{invoice_accessors_common, invoice_builder_methods_common};
122122
#[cfg(test)]
123123
use crate::offers::invoice_macros::invoice_builder_methods_test;
124-
use crate::offers::invoice_request::{INVOICE_REQUEST_PAYER_ID_TYPE, INVOICE_REQUEST_TYPES, IV_BYTES as INVOICE_REQUEST_IV_BYTES, InvoiceRequest, InvoiceRequestContents, InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef};
125-
use crate::offers::merkle::{SignError, SignFn, SignatureTlvStream, SignatureTlvStreamRef, TaggedHash, TlvStream, WithoutSignatures, self};
124+
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};
125+
use crate::offers::merkle::{SignError, SignFn, SignatureTlvStream, SignatureTlvStreamRef, TaggedHash, TlvStream, self, SIGNATURE_TLV_RECORD_SIZE};
126126
use crate::offers::nonce::Nonce;
127-
use crate::offers::offer::{Amount, OFFER_TYPES, OfferTlvStream, OfferTlvStreamRef, Quantity};
127+
use crate::offers::offer::{Amount, EXPERIMENTAL_OFFER_TYPES, OFFER_TYPES, OfferTlvStream, OfferTlvStreamRef, Quantity};
128128
use crate::offers::parse::{Bolt12ParseError, Bolt12SemanticError, ParsedMessage};
129129
use crate::offers::payer::{PAYER_METADATA_TYPE, PayerTlvStream, PayerTlvStreamRef};
130130
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> {
461461
#[derive(Clone)]
462462
pub struct UnsignedBolt12Invoice {
463463
bytes: Vec<u8>,
464+
experimental_bytes: Vec<u8>,
464465
contents: InvoiceContents,
465466
tagged_hash: TaggedHash,
466467
}
@@ -491,19 +492,55 @@ where
491492

492493
impl UnsignedBolt12Invoice {
493494
fn new(invreq_bytes: &[u8], contents: InvoiceContents) -> Self {
495+
// TLV record ranges applicable to invreq_bytes.
496+
const NON_EXPERIMENTAL_TYPES: core::ops::Range<u64> = 0..INVOICE_REQUEST_TYPES.end;
497+
const EXPERIMENTAL_TYPES: core::ops::Range<u64> =
498+
EXPERIMENTAL_OFFER_TYPES.start..EXPERIMENTAL_INVOICE_REQUEST_TYPES.end;
499+
500+
let (_, _, _, invoice_tlv_stream) = contents.as_tlv_stream();
501+
502+
// Allocate enough space for the invoice, which will include:
503+
// - all TLV records from `invreq_bytes` except signatures,
504+
// - all invoice-specific TLV records, and
505+
// - a signature TLV record once the invoice is signed.
506+
//
507+
// This assumes both the invoice request and the invoice will each only have one signature
508+
// using SIGNATURE_TYPES.start as the TLV record. Thus, it is accounted for by invreq_bytes.
509+
let mut bytes = Vec::with_capacity(
510+
invreq_bytes.len()
511+
+ invoice_tlv_stream.serialized_length()
512+
+ if contents.is_for_offer() { 0 } else { SIGNATURE_TLV_RECORD_SIZE }
513+
);
514+
494515
// Use the invoice_request bytes instead of the invoice_request TLV stream as the latter may
495516
// have contained unknown TLV records, which are not stored in `InvoiceRequestContents` or
496517
// `RefundContents`.
497-
let (_, _, _, invoice_tlv_stream) = contents.as_tlv_stream();
498-
let invoice_request_bytes = WithoutSignatures(invreq_bytes);
499-
let unsigned_tlv_stream = (invoice_request_bytes, invoice_tlv_stream);
518+
for record in TlvStream::new(invreq_bytes).range(NON_EXPERIMENTAL_TYPES) {
519+
record.write(&mut bytes).unwrap();
520+
}
500521

501-
let mut bytes = Vec::new();
502-
unsigned_tlv_stream.write(&mut bytes).unwrap();
522+
let remaining_bytes = &invreq_bytes[bytes.len()..];
503523

504-
let tagged_hash = TaggedHash::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &bytes);
524+
invoice_tlv_stream.write(&mut bytes).unwrap();
525+
526+
let mut experimental_tlv_stream = TlvStream::new(remaining_bytes)
527+
.range(EXPERIMENTAL_TYPES)
528+
.peekable();
529+
let mut experimental_bytes = Vec::with_capacity(
530+
remaining_bytes.len()
531+
- experimental_tlv_stream
532+
.peek()
533+
.map_or(remaining_bytes.len(), |first_record| first_record.start)
534+
);
505535

506-
Self { bytes, contents, tagged_hash }
536+
for record in experimental_tlv_stream {
537+
record.write(&mut experimental_bytes).unwrap();
538+
}
539+
540+
let tlv_stream = TlvStream::new(&bytes).chain(TlvStream::new(&experimental_bytes));
541+
let tagged_hash = TaggedHash::from_tlv_stream(SIGNATURE_TAG, tlv_stream);
542+
543+
Self { bytes, experimental_bytes, contents, tagged_hash }
507544
}
508545

509546
/// Returns the [`TaggedHash`] of the invoice to sign.
@@ -528,6 +565,9 @@ macro_rules! unsigned_invoice_sign_method { ($self: ident, $self_type: ty $(, $s
528565
};
529566
signature_tlv_stream.write(&mut $self.bytes).unwrap();
530567

568+
// Append the experimental bytes after the signature.
569+
WithoutLength(&$self.experimental_bytes).write(&mut $self.bytes).unwrap();
570+
531571
Ok(Bolt12Invoice {
532572
#[cfg(not(c_bindings))]
533573
bytes: $self.bytes,
@@ -882,6 +922,13 @@ impl Hash for Bolt12Invoice {
882922
}
883923

884924
impl InvoiceContents {
925+
fn is_for_offer(&self) -> bool {
926+
match self {
927+
InvoiceContents::ForOffer { .. } => true,
928+
InvoiceContents::ForRefund { .. } => false,
929+
}
930+
}
931+
885932
/// Whether the original offer or refund has expired.
886933
#[cfg(feature = "std")]
887934
fn is_offer_or_refund_expired(&self) -> bool {
@@ -1211,7 +1258,7 @@ impl TryFrom<Vec<u8>> for UnsignedBolt12Invoice {
12111258

12121259
fn try_from(bytes: Vec<u8>) -> Result<Self, Self::Error> {
12131260
let invoice = ParsedMessage::<PartialInvoiceTlvStream>::try_from(bytes)?;
1214-
let ParsedMessage { bytes, tlv_stream } = invoice;
1261+
let ParsedMessage { mut bytes, tlv_stream } = invoice;
12151262
let (
12161263
payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream,
12171264
) = tlv_stream;
@@ -1221,7 +1268,13 @@ impl TryFrom<Vec<u8>> for UnsignedBolt12Invoice {
12211268

12221269
let tagged_hash = TaggedHash::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &bytes);
12231270

1224-
Ok(UnsignedBolt12Invoice { bytes, contents, tagged_hash })
1271+
let offset = TlvStream::new(&bytes)
1272+
.range(0..INVOICE_TYPES.end)
1273+
.last()
1274+
.map_or(0, |last_record| last_record.end);
1275+
let experimental_bytes = bytes.split_off(offset);
1276+
1277+
Ok(UnsignedBolt12Invoice { bytes, experimental_bytes, contents, tagged_hash })
12251278
}
12261279
}
12271280

lightning/src/offers/invoice_request.rs

Lines changed: 58 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,9 @@ use crate::ln::channelmanager::PaymentId;
6969
use crate::ln::features::InvoiceRequestFeatures;
7070
use crate::ln::inbound_payment::{ExpandedKey, IV_LEN};
7171
use crate::ln::msgs::DecodeError;
72-
use crate::offers::merkle::{SignError, SignFn, SignatureTlvStream, SignatureTlvStreamRef, TaggedHash, self};
72+
use crate::offers::merkle::{SignError, SignFn, SignatureTlvStream, SignatureTlvStreamRef, TaggedHash, TlvStream, self, SIGNATURE_TLV_RECORD_SIZE};
7373
use crate::offers::nonce::Nonce;
74-
use crate::offers::offer::{Offer, OfferContents, OfferId, OfferTlvStream, OfferTlvStreamRef};
74+
use crate::offers::offer::{EXPERIMENTAL_OFFER_TYPES, OFFER_TYPES, Offer, OfferContents, OfferId, OfferTlvStream, OfferTlvStreamRef};
7575
use crate::offers::parse::{Bolt12ParseError, ParsedMessage, Bolt12SemanticError};
7676
use crate::offers::payer::{PayerContents, PayerTlvStream, PayerTlvStreamRef};
7777
use crate::offers::signer::{Metadata, MetadataMaterial};
@@ -488,6 +488,7 @@ for InvoiceRequestBuilder<'a, 'b, DerivedPayerSigningPubkey, secp256k1::All> {
488488
#[derive(Clone)]
489489
pub struct UnsignedInvoiceRequest {
490490
bytes: Vec<u8>,
491+
experimental_bytes: Vec<u8>,
491492
contents: InvoiceRequestContents,
492493
tagged_hash: TaggedHash,
493494
}
@@ -520,17 +521,49 @@ impl UnsignedInvoiceRequest {
520521
fn new(offer: &Offer, contents: InvoiceRequestContents) -> Self {
521522
// Use the offer bytes instead of the offer TLV stream as the offer may have contained
522523
// unknown TLV records, which are not stored in `OfferContents`.
523-
let (payer_tlv_stream, _offer_tlv_stream, invoice_request_tlv_stream) =
524-
contents.as_tlv_stream();
525-
let offer_bytes = WithoutLength(&offer.bytes);
526-
let unsigned_tlv_stream = (payer_tlv_stream, offer_bytes, invoice_request_tlv_stream);
524+
let (
525+
payer_tlv_stream, _offer_tlv_stream, invoice_request_tlv_stream,
526+
) = contents.as_tlv_stream();
527+
528+
// Allocate enough space for the invoice_request, which will include:
529+
// - all TLV records from `offer.bytes`,
530+
// - all invoice_request-specific TLV records, and
531+
// - a signature TLV record once the invoice_request is signed.
532+
let mut bytes = Vec::with_capacity(
533+
offer.bytes.len()
534+
+ payer_tlv_stream.serialized_length()
535+
+ invoice_request_tlv_stream.serialized_length()
536+
+ SIGNATURE_TLV_RECORD_SIZE
537+
);
527538

528-
let mut bytes = Vec::new();
529-
unsigned_tlv_stream.write(&mut bytes).unwrap();
539+
payer_tlv_stream.write(&mut bytes).unwrap();
530540

531-
let tagged_hash = TaggedHash::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &bytes);
541+
for record in TlvStream::new(&offer.bytes).range(OFFER_TYPES) {
542+
record.write(&mut bytes).unwrap();
543+
}
544+
545+
let remaining_bytes = &offer.bytes[bytes.len() - payer_tlv_stream.serialized_length()..];
546+
547+
invoice_request_tlv_stream.write(&mut bytes).unwrap();
548+
549+
let mut experimental_tlv_stream = TlvStream::new(remaining_bytes)
550+
.range(EXPERIMENTAL_OFFER_TYPES)
551+
.peekable();
552+
let mut experimental_bytes = Vec::with_capacity(
553+
remaining_bytes.len()
554+
- experimental_tlv_stream
555+
.peek()
556+
.map_or(remaining_bytes.len(), |first_record| first_record.start)
557+
);
558+
559+
for record in experimental_tlv_stream {
560+
record.write(&mut experimental_bytes).unwrap();
561+
}
562+
563+
let tlv_stream = TlvStream::new(&bytes).chain(TlvStream::new(&experimental_bytes));
564+
let tagged_hash = TaggedHash::from_tlv_stream(SIGNATURE_TAG, tlv_stream);
532565

533-
Self { bytes, contents, tagged_hash }
566+
Self { bytes, experimental_bytes, contents, tagged_hash }
534567
}
535568

536569
/// Returns the [`TaggedHash`] of the invoice to sign.
@@ -557,6 +590,9 @@ macro_rules! unsigned_invoice_request_sign_method { (
557590
};
558591
signature_tlv_stream.write(&mut $self.bytes).unwrap();
559592

593+
// Append the experimental bytes after the signature.
594+
WithoutLength(&$self.experimental_bytes).write(&mut $self.bytes).unwrap();
595+
560596
Ok(InvoiceRequest {
561597
#[cfg(not(c_bindings))]
562598
bytes: $self.bytes,
@@ -1072,6 +1108,10 @@ tlv_stream!(InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef<'a>, INVOICE_REQ
10721108
(90, paths: (Vec<BlindedMessagePath>, WithoutLength)),
10731109
});
10741110

1111+
/// Valid type range for experimental invoice_request TLV records.
1112+
pub(super) const EXPERIMENTAL_INVOICE_REQUEST_TYPES: core::ops::Range<u64> =
1113+
2_000_000_000..3_000_000_000;
1114+
10751115
type FullInvoiceRequestTlvStream =
10761116
(PayerTlvStream, OfferTlvStream, InvoiceRequestTlvStream, SignatureTlvStream);
10771117

@@ -1106,7 +1146,7 @@ impl TryFrom<Vec<u8>> for UnsignedInvoiceRequest {
11061146

11071147
fn try_from(bytes: Vec<u8>) -> Result<Self, Self::Error> {
11081148
let invoice_request = ParsedMessage::<PartialInvoiceRequestTlvStream>::try_from(bytes)?;
1109-
let ParsedMessage { bytes, tlv_stream } = invoice_request;
1149+
let ParsedMessage { mut bytes, tlv_stream } = invoice_request;
11101150
let (
11111151
payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream,
11121152
) = tlv_stream;
@@ -1116,7 +1156,13 @@ impl TryFrom<Vec<u8>> for UnsignedInvoiceRequest {
11161156

11171157
let tagged_hash = TaggedHash::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &bytes);
11181158

1119-
Ok(UnsignedInvoiceRequest { bytes, contents, tagged_hash })
1159+
let offset = TlvStream::new(&bytes)
1160+
.range(0..INVOICE_REQUEST_TYPES.end)
1161+
.last()
1162+
.map_or(0, |last_record| last_record.end);
1163+
let experimental_bytes = bytes.split_off(offset);
1164+
1165+
Ok(UnsignedInvoiceRequest { bytes, experimental_bytes, contents, tagged_hash })
11201166
}
11211167
}
11221168

lightning/src/offers/merkle.rs

Lines changed: 20 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
1212
use bitcoin::hashes::{Hash, HashEngine, sha256};
1313
use bitcoin::secp256k1::{Message, PublicKey, Secp256k1, self};
14+
use bitcoin::secp256k1::constants::SCHNORR_SIGNATURE_SIZE;
1415
use bitcoin::secp256k1::schnorr::Signature;
1516
use crate::io;
1617
use crate::util::ser::{BigSize, Readable, Writeable, Writer};
@@ -19,12 +20,16 @@ use crate::util::ser::{BigSize, Readable, Writeable, Writer};
1920
use crate::prelude::*;
2021

2122
/// Valid type range for signature TLV records.
22-
const SIGNATURE_TYPES: core::ops::RangeInclusive<u64> = 240..=1000;
23+
pub(super) const SIGNATURE_TYPES: core::ops::RangeInclusive<u64> = 240..=1000;
2324

2425
tlv_stream!(SignatureTlvStream, SignatureTlvStreamRef<'a>, SIGNATURE_TYPES, {
2526
(240, signature: Signature),
2627
});
2728

29+
/// Size of a TLV record in `SIGNATURE_TYPES` when the type is 1000. TLV types are encoded using
30+
/// BigSize, so a TLV record with type 240 will use two less bytes.
31+
pub(super) const SIGNATURE_TLV_RECORD_SIZE: usize = 3 + 1 + SCHNORR_SIGNATURE_SIZE;
32+
2833
/// A hash for use in a specific context by tweaking with a context-dependent tag as per [BIP 340]
2934
/// and computed over the merkle root of a TLV stream to sign as defined in [BOLT 12].
3035
///
@@ -164,7 +169,7 @@ fn root_hash<'a, I: core::iter::Iterator<Item = TlvRecord<'a>>>(tlv_stream: I) -
164169
let branch_tag = tagged_hash_engine(sha256::Hash::hash("LnBranch".as_bytes()));
165170

166171
let mut leaves = Vec::new();
167-
for record in TlvStream::skip_signatures(tlv_stream) {
172+
for record in tlv_stream.filter(|record| !SIGNATURE_TYPES.contains(&record.r#type)) {
168173
leaves.push(tagged_hash_from_engine(leaf_tag.clone(), &record.record_bytes));
169174
leaves.push(tagged_hash_from_engine(nonce_tag.clone(), &record.type_bytes));
170175
}
@@ -240,12 +245,6 @@ impl<'a> TlvStream<'a> {
240245
self.skip_while(move |record| !types.contains(&record.r#type))
241246
.take_while(move |record| take_range.contains(&record.r#type))
242247
}
243-
244-
fn skip_signatures(
245-
tlv_stream: impl core::iter::Iterator<Item = TlvRecord<'a>>
246-
) -> impl core::iter::Iterator<Item = TlvRecord<'a>> {
247-
tlv_stream.filter(|record| !SIGNATURE_TYPES.contains(&record.r#type))
248-
}
249248
}
250249

251250
/// A slice into a [`TlvStream`] for a record.
@@ -254,6 +253,8 @@ pub(super) struct TlvRecord<'a> {
254253
type_bytes: &'a [u8],
255254
// The entire TLV record.
256255
pub(super) record_bytes: &'a [u8],
256+
pub(super) start: usize,
257+
pub(super) end: usize,
257258
}
258259

259260
impl<'a> Iterator for TlvStream<'a> {
@@ -276,32 +277,25 @@ impl<'a> Iterator for TlvStream<'a> {
276277

277278
self.data.set_position(end);
278279

279-
Some(TlvRecord { r#type, type_bytes, record_bytes })
280+
Some(TlvRecord {
281+
r#type, type_bytes, record_bytes, start: start as usize, end: end as usize,
282+
})
280283
} else {
281284
None
282285
}
283286
}
284287
}
285288

286-
/// Encoding for a pre-serialized TLV stream that excludes any signature TLV records.
287-
///
288-
/// Panics if the wrapped bytes are not a well-formed TLV stream.
289-
pub(super) struct WithoutSignatures<'a>(pub &'a [u8]);
290-
291-
impl<'a> Writeable for WithoutSignatures<'a> {
289+
impl<'a> Writeable for TlvRecord<'a> {
292290
#[inline]
293291
fn write<W: Writer>(&self, writer: &mut W) -> Result<(), io::Error> {
294-
let tlv_stream = TlvStream::new(self.0);
295-
for record in TlvStream::skip_signatures(tlv_stream) {
296-
writer.write_all(record.record_bytes)?;
297-
}
298-
Ok(())
292+
writer.write_all(self.record_bytes)
299293
}
300294
}
301295

302296
#[cfg(test)]
303297
mod tests {
304-
use super::{SIGNATURE_TYPES, TlvStream, WithoutSignatures};
298+
use super::{SIGNATURE_TYPES, TlvStream};
305299

306300
use bitcoin::hashes::{Hash, sha256};
307301
use bitcoin::hex::FromHex;
@@ -411,7 +405,11 @@ mod tests {
411405
.unwrap();
412406

413407
let mut bytes_without_signature = Vec::new();
414-
WithoutSignatures(&invoice_request.bytes).write(&mut bytes_without_signature).unwrap();
408+
let tlv_stream_without_signatures = TlvStream::new(&invoice_request.bytes)
409+
.filter(|record| !SIGNATURE_TYPES.contains(&record.r#type));
410+
for record in tlv_stream_without_signatures {
411+
record.write(&mut bytes_without_signature).unwrap();
412+
}
415413

416414
assert_ne!(bytes_without_signature, invoice_request.bytes);
417415
assert_eq!(

lightning/src/offers/offer.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1091,6 +1091,9 @@ tlv_stream!(OfferTlvStream, OfferTlvStreamRef<'a>, OFFER_TYPES, {
10911091
(OFFER_ISSUER_ID_TYPE, issuer_id: PublicKey),
10921092
});
10931093

1094+
/// Valid type range for experimental offer TLV records.
1095+
pub(super) const EXPERIMENTAL_OFFER_TYPES: core::ops::Range<u64> = 1_000_000_000..2_000_000_000;
1096+
10941097
impl Bech32Encode for Offer {
10951098
const BECH32_HRP: &'static str = "lno";
10961099
}

0 commit comments

Comments
 (0)