diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index 844018012c1..b1d7660f224 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -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, ExperimentalInvoiceRequestTlvStream, ExperimentalInvoiceRequestTlvStreamRef, 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, ExperimentalOfferTlvStream, ExperimentalOfferTlvStreamRef, 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}; @@ -363,6 +363,8 @@ macro_rules! invoice_builder_methods { ( InvoiceFields { payment_paths, created_at, relative_expiry: None, payment_hash, amount_msats, fallbacks: None, features: Bolt12InvoiceFeatures::empty(), signing_pubkey, + #[cfg(test)] + experimental_baz: None, } } @@ -461,6 +463,7 @@ for InvoiceBuilder<'a, DerivedSigningPubkey> { #[derive(Clone)] pub struct UnsignedBolt12Invoice { bytes: Vec, + experimental_bytes: Vec, contents: InvoiceContents, tagged_hash: TaggedHash, } @@ -491,19 +494,61 @@ 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 = 0..INVOICE_REQUEST_TYPES.end; + const EXPERIMENTAL_TYPES: core::ops::Range = + EXPERIMENTAL_OFFER_TYPES.start..EXPERIMENTAL_INVOICE_REQUEST_TYPES.end; + + let (_, _, _, invoice_tlv_stream, _, _, experimental_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 } + + experimental_invoice_tlv_stream.serialized_length(), + ); + // 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(); - Self { bytes, contents, tagged_hash } + 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) + + experimental_invoice_tlv_stream.serialized_length(), + ); + + for record in experimental_tlv_stream { + record.write(&mut experimental_bytes).unwrap(); + } + + experimental_invoice_tlv_stream.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 } } /// Returns the [`TaggedHash`] of the invoice to sign. @@ -528,6 +573,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, @@ -612,6 +668,8 @@ struct InvoiceFields { fallbacks: Option>, features: Bolt12InvoiceFeatures, signing_pubkey: PublicKey, + #[cfg(test)] + experimental_baz: Option, } macro_rules! invoice_accessors { ($self: ident, $contents: expr) => { @@ -828,7 +886,7 @@ impl Bolt12Invoice { (&refund.payer.0, REFUND_IV_BYTES_WITH_METADATA) }, }; - self.contents.verify(TlvStream::new(&self.bytes), metadata, key, iv_bytes, secp_ctx) + self.contents.verify(&self.bytes, metadata, key, iv_bytes, secp_ctx) } /// Verifies that the invoice was for a request or refund created using the given key by @@ -842,7 +900,8 @@ impl Bolt12Invoice { InvoiceContents::ForOffer { .. } => INVOICE_REQUEST_IV_BYTES, InvoiceContents::ForRefund { .. } => REFUND_IV_BYTES_WITHOUT_METADATA, }; - self.contents.verify(TlvStream::new(&self.bytes), &metadata, key, iv_bytes, secp_ctx) + self.contents + .verify(&self.bytes, &metadata, key, iv_bytes, secp_ctx) .and_then(|extracted_payment_id| (payment_id == extracted_payment_id) .then(|| payment_id) .ok_or(()) @@ -850,13 +909,19 @@ impl Bolt12Invoice { } pub(crate) fn as_tlv_stream(&self) -> FullInvoiceTlvStreamRef { - let (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream) = - self.contents.as_tlv_stream(); + 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) + ( + 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_tlv_stream, + ) } pub(crate) fn is_for_refund_without_paths(&self) -> bool { @@ -882,6 +947,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 { @@ -1086,18 +1158,22 @@ impl InvoiceContents { } fn verify( - &self, tlv_stream: TlvStream<'_>, metadata: &Metadata, key: &ExpandedKey, - iv_bytes: &[u8; IV_LEN], secp_ctx: &Secp256k1 + &self, bytes: &[u8], metadata: &Metadata, key: &ExpandedKey, iv_bytes: &[u8; IV_LEN], + secp_ctx: &Secp256k1, ) -> Result { - let offer_records = tlv_stream.clone().range(OFFER_TYPES); - let invreq_records = tlv_stream.range(INVOICE_REQUEST_TYPES).filter(|record| { + const EXPERIMENTAL_TYPES: core::ops::Range = + EXPERIMENTAL_OFFER_TYPES.start..EXPERIMENTAL_INVOICE_REQUEST_TYPES.end; + + let offer_records = TlvStream::new(bytes).range(OFFER_TYPES); + let invreq_records = TlvStream::new(bytes).range(INVOICE_REQUEST_TYPES).filter(|record| { match record.r#type { PAYER_METADATA_TYPE => false, // Should be outside range INVOICE_REQUEST_PAYER_ID_TYPE => !metadata.derives_payer_keys(), _ => true, } }); - let tlv_stream = offer_records.chain(invreq_records); + let experimental_records = TlvStream::new(bytes).range(EXPERIMENTAL_TYPES); + let tlv_stream = offer_records.chain(invreq_records).chain(experimental_records); let signing_pubkey = self.payer_signing_pubkey(); signer::verify_payer_metadata( @@ -1106,13 +1182,18 @@ impl InvoiceContents { } fn as_tlv_stream(&self) -> PartialInvoiceTlvStreamRef { - let (payer, offer, invoice_request) = match self { + let ( + payer, offer, invoice_request, experimental_offer, experimental_invoice_request, + ) = match self { 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) + ( + payer, offer, invoice_request, invoice, experimental_offer, + experimental_invoice_request, experimental_invoice, + ) } } @@ -1160,24 +1241,30 @@ 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 { + #[cfg(test)] + experimental_baz: self.experimental_baz, + }, + ) } } @@ -1211,17 +1298,18 @@ impl TryFrom> for UnsignedBolt12Invoice { fn try_from(bytes: Vec) -> Result { let invoice = ParsedMessage::::try_from(bytes)?; - let ParsedMessage { bytes, tlv_stream } = invoice; - let ( - payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream, - ) = tlv_stream; - let contents = InvoiceContents::try_from( - (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream) - )?; + let ParsedMessage { mut bytes, tlv_stream } = invoice; + let contents = InvoiceContents::try_from(tlv_stream)?; 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 }) } } @@ -1234,7 +1322,10 @@ impl TryFrom> for Bolt12Invoice { } } -tlv_stream!(InvoiceTlvStream, InvoiceTlvStreamRef, 160..240, { +/// Valid type range for invoice TLV records. +pub(super) const INVOICE_TYPES: core::ops::Range = 160..240; + +tlv_stream!(InvoiceTlvStream, InvoiceTlvStreamRef<'a>, INVOICE_TYPES, { (160, paths: (Vec, WithoutLength, Iterable<'a, BlindedPathIter<'a>, BlindedPath>)), (162, blindedpay: (Vec, WithoutLength, Iterable<'a, BlindedPayInfoIter<'a>, BlindedPayInfo>)), (164, created_at: (u64, HighZeroBytesDroppedBigSize)), @@ -1245,9 +1336,24 @@ tlv_stream!(InvoiceTlvStream, InvoiceTlvStreamRef, 160..240, { (174, features: (Bolt12InvoiceFeatures, WithoutLength)), (176, node_id: PublicKey), // Only present in `StaticInvoice`s. - (238, message_paths: (Vec, WithoutLength)), + (236, message_paths: (Vec, WithoutLength)), }); +/// Valid type range for experimental invoice TLV records. +pub(super) const EXPERIMENTAL_INVOICE_TYPES: core::ops::RangeFrom = 3_000_000_000..; + +#[cfg(not(test))] +tlv_stream!( + ExperimentalInvoiceTlvStream, ExperimentalInvoiceTlvStreamRef, EXPERIMENTAL_INVOICE_TYPES, {} +); + +#[cfg(test)] +tlv_stream!( + ExperimentalInvoiceTlvStream, ExperimentalInvoiceTlvStreamRef, EXPERIMENTAL_INVOICE_TYPES, { + (3_999_999_999, experimental_baz: (u64, HighZeroBytesDroppedBigSize)), + } +); + pub(super) type BlindedPathIter<'a> = core::iter::Map< core::slice::Iter<'a, BlindedPaymentPath>, for<'r> fn(&'r BlindedPaymentPath) -> &'r BlindedPath, @@ -1267,8 +1373,10 @@ pub(super) struct FallbackAddress { impl_writeable!(FallbackAddress, { version, program }); -type FullInvoiceTlvStream = - (PayerTlvStream, OfferTlvStream, InvoiceRequestTlvStream, InvoiceTlvStream, SignatureTlvStream); +type FullInvoiceTlvStream =( + PayerTlvStream, OfferTlvStream, InvoiceRequestTlvStream, InvoiceTlvStream, SignatureTlvStream, + ExperimentalOfferTlvStream, ExperimentalInvoiceRequestTlvStream, ExperimentalInvoiceTlvStream, +); type FullInvoiceTlvStreamRef<'a> = ( PayerTlvStreamRef<'a>, @@ -1276,6 +1384,9 @@ type FullInvoiceTlvStreamRef<'a> = ( InvoiceRequestTlvStreamRef<'a>, InvoiceTlvStreamRef<'a>, SignatureTlvStreamRef<'a>, + ExperimentalOfferTlvStreamRef, + ExperimentalInvoiceRequestTlvStreamRef, + ExperimentalInvoiceTlvStreamRef, ); impl CursorReadable for FullInvoiceTlvStream { @@ -1285,19 +1396,32 @@ impl CursorReadable for FullInvoiceTlvStream { let invoice_request = CursorReadable::read(r)?; let invoice = CursorReadable::read(r)?; 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)) + Ok( + ( + payer, offer, invoice_request, invoice, signature, experimental_offer, + experimental_invoice_request, experimental_invoice, + ) + ) } } -type PartialInvoiceTlvStream = - (PayerTlvStream, OfferTlvStream, InvoiceRequestTlvStream, InvoiceTlvStream); +type PartialInvoiceTlvStream = ( + PayerTlvStream, OfferTlvStream, InvoiceRequestTlvStream, InvoiceTlvStream, + ExperimentalOfferTlvStream, ExperimentalInvoiceRequestTlvStream, ExperimentalInvoiceTlvStream, +); type PartialInvoiceTlvStreamRef<'a> = ( PayerTlvStreamRef<'a>, OfferTlvStreamRef<'a>, InvoiceRequestTlvStreamRef<'a>, InvoiceTlvStreamRef<'a>, + ExperimentalOfferTlvStreamRef, + ExperimentalInvoiceRequestTlvStreamRef, + ExperimentalInvoiceTlvStreamRef, ); impl CursorReadable for PartialInvoiceTlvStream { @@ -1306,8 +1430,16 @@ impl CursorReadable for PartialInvoiceTlvStream { let offer = CursorReadable::read(r)?; let invoice_request = CursorReadable::read(r)?; 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)) + Ok( + ( + payer, offer, invoice_request, invoice, experimental_offer, + experimental_invoice_request, experimental_invoice, + ) + ) } } @@ -1319,9 +1451,16 @@ impl TryFrom> for Bolt12Invoice { let ( payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream, 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) + ( + 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, + ) )?; let signature = signature.ok_or( @@ -1347,6 +1486,12 @@ impl TryFrom for InvoiceContents { paths, blindedpay, created_at, relative_expiry, payment_hash, amount, fallbacks, features, node_id, message_paths, }, + experimental_offer_tlv_stream, + experimental_invoice_request_tlv_stream, + ExperimentalInvoiceTlvStream { + #[cfg(test)] + experimental_baz, + }, ) = tlv_stream; if message_paths.is_some() { return Err(Bolt12SemanticError::UnexpectedPaths) } @@ -1373,18 +1518,26 @@ impl TryFrom for InvoiceContents { let fields = InvoiceFields { payment_paths, created_at, relative_expiry, payment_hash, amount_msats, fallbacks, features, signing_pubkey, + #[cfg(test)] + experimental_baz, }; check_invoice_signing_pubkey(&fields.signing_pubkey, &offer_tlv_stream)?; if offer_tlv_stream.issuer_id.is_none() && offer_tlv_stream.paths.is_none() { let refund = RefundContents::try_from( - (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream) + ( + payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, + experimental_offer_tlv_stream, experimental_invoice_request_tlv_stream, + ) )?; Ok(InvoiceContents::ForRefund { refund, fields }) } else { let invoice_request = InvoiceRequestContents::try_from( - (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream) + ( + payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, + experimental_offer_tlv_stream, experimental_invoice_request_tlv_stream, + ) )?; Ok(InvoiceContents::ForOffer { invoice_request, fields }) } @@ -1437,7 +1590,7 @@ pub(super) fn check_invoice_signing_pubkey( #[cfg(test)] mod tests { - use super::{Bolt12Invoice, DEFAULT_RELATIVE_EXPIRY, FallbackAddress, FullInvoiceTlvStreamRef, InvoiceTlvStreamRef, SIGNATURE_TAG, UnsignedBolt12Invoice}; + use super::{Bolt12Invoice, DEFAULT_RELATIVE_EXPIRY, EXPERIMENTAL_INVOICE_TYPES, ExperimentalInvoiceTlvStreamRef, FallbackAddress, FullInvoiceTlvStreamRef, INVOICE_TYPES, InvoiceTlvStreamRef, SIGNATURE_TAG, UnsignedBolt12Invoice}; use bitcoin::{CompressedPublicKey, WitnessProgram, WitnessVersion}; use bitcoin::constants::ChainHash; @@ -1456,10 +1609,10 @@ mod tests { use crate::types::features::{Bolt12InvoiceFeatures, InvoiceRequestFeatures, OfferFeatures}; use crate::ln::inbound_payment::ExpandedKey; use crate::ln::msgs::DecodeError; - use crate::offers::invoice_request::InvoiceRequestTlvStreamRef; - use crate::offers::merkle::{SignError, SignatureTlvStreamRef, TaggedHash, self}; + use crate::offers::invoice_request::{ExperimentalInvoiceRequestTlvStreamRef, InvoiceRequestTlvStreamRef}; + use crate::offers::merkle::{SignError, SignatureTlvStreamRef, TaggedHash, TlvStream, self}; use crate::offers::nonce::Nonce; - use crate::offers::offer::{Amount, OfferTlvStreamRef, Quantity}; + use crate::offers::offer::{Amount, ExperimentalOfferTlvStreamRef, OfferTlvStreamRef, Quantity}; use crate::prelude::*; #[cfg(not(c_bindings))] use { @@ -1627,6 +1780,15 @@ mod tests { message_paths: None, }, SignatureTlvStreamRef { signature: Some(&invoice.signature()) }, + ExperimentalOfferTlvStreamRef { + experimental_foo: None, + }, + ExperimentalInvoiceRequestTlvStreamRef { + experimental_bar: None, + }, + ExperimentalInvoiceTlvStreamRef { + experimental_baz: None, + }, ), ); @@ -1720,6 +1882,15 @@ mod tests { message_paths: None, }, SignatureTlvStreamRef { signature: Some(&invoice.signature()) }, + ExperimentalOfferTlvStreamRef { + experimental_foo: None, + }, + ExperimentalInvoiceRequestTlvStreamRef { + experimental_bar: None, + }, + ExperimentalInvoiceTlvStreamRef { + experimental_baz: None, + }, ), ); @@ -1813,6 +1984,7 @@ mod tests { let offer = OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, nonce, &secp_ctx) .amount_msats(1000) .path(blinded_path) + .experimental_foo(42) .build().unwrap(); let invoice_request = offer.request_invoice(vec![1; 32], payer_pubkey()).unwrap() .build().unwrap() @@ -1834,6 +2006,7 @@ mod tests { let offer = OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, nonce, &secp_ctx) .amount_msats(1000) // Omit the path so that node_id is used for the signing pubkey instead of deriving it + .experimental_foo(42) .build().unwrap(); let invoice_request = offer.request_invoice(vec![1; 32], payer_pubkey()).unwrap() .build().unwrap() @@ -1855,6 +2028,7 @@ mod tests { let secp_ctx = Secp256k1::new(); let refund = RefundBuilder::new(vec![1; 32], payer_pubkey(), 1000).unwrap() + .experimental_foo(42) .build().unwrap(); if let Err(e) = refund @@ -1913,7 +2087,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); @@ -1929,7 +2103,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)); @@ -1948,7 +2122,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)); } @@ -1966,7 +2140,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)); @@ -2004,7 +2178,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![ @@ -2047,7 +2221,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)); } @@ -2494,7 +2668,217 @@ mod tests { } #[test] - fn fails_parsing_invoice_with_extra_tlv_records() { + fn parses_invoice_with_unknown_tlv_records() { + const UNKNOWN_ODD_TYPE: u64 = INVOICE_TYPES.end - 1; + assert!(UNKNOWN_ODD_TYPE % 2 == 1); + + let secp_ctx = Secp256k1::new(); + let keys = Keypair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap()); + let mut unsigned_invoice = OfferBuilder::new(keys.public_key()) + .amount_msats(1000) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .build().unwrap() + .sign(payer_sign).unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() + .build().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); + + let invoice = unsigned_invoice + .sign(|message: &UnsignedBolt12Invoice| + Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys)) + ) + .unwrap(); + + let mut encoded_invoice = Vec::new(); + invoice.write(&mut encoded_invoice).unwrap(); + + match Bolt12Invoice::try_from(encoded_invoice.clone()) { + Ok(invoice) => assert_eq!(invoice.bytes, encoded_invoice), + Err(e) => panic!("error parsing invoice: {:?}", e), + } + + const UNKNOWN_EVEN_TYPE: u64 = INVOICE_TYPES.end - 2; + assert!(UNKNOWN_EVEN_TYPE % 2 == 0); + + let mut unsigned_invoice = OfferBuilder::new(keys.public_key()) + .amount_msats(1000) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .build().unwrap() + .sign(payer_sign).unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() + .build().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); + + let invoice = unsigned_invoice + .sign(|message: &UnsignedBolt12Invoice| + Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys)) + ) + .unwrap(); + + let mut encoded_invoice = Vec::new(); + invoice.write(&mut encoded_invoice).unwrap(); + + match Bolt12Invoice::try_from(encoded_invoice) { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, Bolt12ParseError::Decode(DecodeError::UnknownRequiredFeature)), + } + } + + #[test] + fn parses_invoice_with_experimental_tlv_records() { + let secp_ctx = Secp256k1::new(); + let keys = Keypair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap()); + let invoice = OfferBuilder::new(keys.public_key()) + .amount_msats(1000) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .build().unwrap() + .sign(payer_sign).unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() + .experimental_baz(42) + .build().unwrap() + .sign(|message: &UnsignedBolt12Invoice| + Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys)) + ) + .unwrap(); + + let mut encoded_invoice = Vec::new(); + invoice.write(&mut encoded_invoice).unwrap(); + + assert!(Bolt12Invoice::try_from(encoded_invoice).is_ok()); + + const UNKNOWN_ODD_TYPE: u64 = EXPERIMENTAL_INVOICE_TYPES.start + 1; + assert!(UNKNOWN_ODD_TYPE % 2 == 1); + + let mut unsigned_invoice = OfferBuilder::new(keys.public_key()) + .amount_msats(1000) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .build().unwrap() + .sign(payer_sign).unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() + .build().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.experimental_bytes.extend_from_slice(&unknown_bytes); + + let tlv_stream = TlvStream::new(&unsigned_invoice.bytes) + .chain(TlvStream::new(&unsigned_invoice.experimental_bytes)); + unsigned_invoice.tagged_hash = TaggedHash::from_tlv_stream(SIGNATURE_TAG, tlv_stream); + + let invoice = unsigned_invoice + .sign(|message: &UnsignedBolt12Invoice| + Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys)) + ) + .unwrap(); + + let mut encoded_invoice = Vec::new(); + invoice.write(&mut encoded_invoice).unwrap(); + + match Bolt12Invoice::try_from(encoded_invoice.clone()) { + Ok(invoice) => assert_eq!(invoice.bytes, encoded_invoice), + Err(e) => panic!("error parsing invoice: {:?}", e), + } + + const UNKNOWN_EVEN_TYPE: u64 = EXPERIMENTAL_INVOICE_TYPES.start; + assert!(UNKNOWN_EVEN_TYPE % 2 == 0); + + let mut unsigned_invoice = OfferBuilder::new(keys.public_key()) + .amount_msats(1000) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .build().unwrap() + .sign(payer_sign).unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() + .build().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.experimental_bytes.extend_from_slice(&unknown_bytes); + + let tlv_stream = TlvStream::new(&unsigned_invoice.bytes) + .chain(TlvStream::new(&unsigned_invoice.experimental_bytes)); + unsigned_invoice.tagged_hash = TaggedHash::from_tlv_stream(SIGNATURE_TAG, tlv_stream); + + let invoice = unsigned_invoice + .sign(|message: &UnsignedBolt12Invoice| + Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys)) + ) + .unwrap(); + + let mut encoded_invoice = Vec::new(); + invoice.write(&mut encoded_invoice).unwrap(); + + match Bolt12Invoice::try_from(encoded_invoice) { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, Bolt12ParseError::Decode(DecodeError::UnknownRequiredFeature)), + } + + let invoice = OfferBuilder::new(keys.public_key()) + .amount_msats(1000) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .build().unwrap() + .sign(payer_sign).unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() + .build().unwrap() + .sign(|message: &UnsignedBolt12Invoice| + Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys)) + ) + .unwrap(); + + let mut encoded_invoice = Vec::new(); + invoice.write(&mut encoded_invoice).unwrap(); + + BigSize(UNKNOWN_ODD_TYPE).write(&mut encoded_invoice).unwrap(); + BigSize(32).write(&mut encoded_invoice).unwrap(); + [42u8; 32].write(&mut encoded_invoice).unwrap(); + + match Bolt12Invoice::try_from(encoded_invoice) { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, Bolt12ParseError::InvalidSignature(secp256k1::Error::IncorrectSignature)), + } + } + + #[test] + fn fails_parsing_invoice_with_out_of_range_tlv_records() { let invoice = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .build().unwrap() diff --git a/lightning/src/offers/invoice_macros.rs b/lightning/src/offers/invoice_macros.rs index 579ecd2d20a..4a540c16046 100644 --- a/lightning/src/offers/invoice_macros.rs +++ b/lightning/src/offers/invoice_macros.rs @@ -95,6 +95,11 @@ macro_rules! invoice_builder_methods_test { ( $return_value } + #[cfg_attr(c_bindings, allow(dead_code))] + pub(super) fn experimental_baz($($self_mut)* $self: $self_type, experimental_baz: u64) -> $return_type { + $invoice_fields.experimental_baz = Some(experimental_baz); + $return_value + } } } macro_rules! invoice_accessors_common { ($self: ident, $contents: expr, $invoice_type: ty) => { diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index fed8bfee27f..d1ab6d067d9 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -69,9 +69,9 @@ use crate::ln::channelmanager::PaymentId; use crate::types::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, ExperimentalOfferTlvStream, ExperimentalOfferTlvStreamRef, 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}; @@ -241,6 +241,8 @@ macro_rules! invoice_request_builder_methods { ( InvoiceRequestContentsWithoutPayerSigningPubkey { payer: PayerContents(metadata), offer, chain: None, amount_msats: None, features: InvoiceRequestFeatures::empty(), quantity: None, payer_note: None, + #[cfg(test)] + experimental_bar: None, } } @@ -404,6 +406,12 @@ macro_rules! invoice_request_builder_test_methods { ( $return_value } + #[cfg_attr(c_bindings, allow(dead_code))] + pub(super) fn experimental_bar($($self_mut)* $self: $self_type, experimental_bar: u64) -> $return_type { + $self.invoice_request.experimental_bar = Some(experimental_bar); + $return_value + } + #[cfg_attr(c_bindings, allow(dead_code))] pub(super) fn build_unchecked($self: $self_type) -> UnsignedInvoiceRequest { $self.build_without_checks().0 @@ -488,6 +496,7 @@ for InvoiceRequestBuilder<'a, 'b, DerivedPayerSigningPubkey, secp256k1::All> { #[derive(Clone)] pub struct UnsignedInvoiceRequest { bytes: Vec, + experimental_bytes: Vec, contents: InvoiceRequestContents, tagged_hash: TaggedHash, } @@ -520,17 +529,55 @@ 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, + _experimental_offer_tlv_stream, experimental_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 + + experimental_invoice_request_tlv_stream.serialized_length(), + ); - 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(); + } - Self { bytes, contents, tagged_hash } + 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) + + experimental_invoice_request_tlv_stream.serialized_length(), + ); + + for record in experimental_tlv_stream { + record.write(&mut experimental_bytes).unwrap(); + } + + experimental_invoice_request_tlv_stream.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 } } /// Returns the [`TaggedHash`] of the invoice to sign. @@ -557,6 +604,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, @@ -643,6 +699,8 @@ pub(super) struct InvoiceRequestContentsWithoutPayerSigningPubkey { features: InvoiceRequestFeatures, quantity: Option, payer_note: Option, + #[cfg(test)] + experimental_bar: Option, } macro_rules! invoice_request_accessors { ($self: ident, $contents: expr) => { @@ -863,12 +921,18 @@ impl InvoiceRequest { } pub(crate) fn as_tlv_stream(&self) -> FullInvoiceRequestTlvStreamRef { - let (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream) = - self.contents.as_tlv_stream(); + let ( + payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, + experimental_offer_tlv_stream, experimental_invoice_request_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, signature_tlv_stream) + ( + payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, + signature_tlv_stream, experimental_offer_tlv_stream, + experimental_invoice_request_tlv_stream, + ) } } @@ -940,7 +1004,9 @@ impl VerifiedInvoiceRequest { let InvoiceRequestContents { payer_signing_pubkey, inner: InvoiceRequestContentsWithoutPayerSigningPubkey { - payer: _, offer: _, chain: _, amount_msats: _, features: _, quantity, payer_note + payer: _, offer: _, chain: _, amount_msats: _, features: _, quantity, payer_note, + #[cfg(test)] + experimental_bar: _, }, } = &self.inner.contents; @@ -984,9 +1050,10 @@ impl InvoiceRequestContents { } pub(super) fn as_tlv_stream(&self) -> PartialInvoiceRequestTlvStreamRef { - let (payer, offer, mut invoice_request) = self.inner.as_tlv_stream(); + let (payer, offer, mut invoice_request, experimental_offer, experimental_invoice_request) = + self.inner.as_tlv_stream(); invoice_request.payer_id = Some(&self.payer_signing_pubkey); - (payer, offer, invoice_request) + (payer, offer, invoice_request, experimental_offer, experimental_invoice_request) } } @@ -1004,7 +1071,7 @@ impl InvoiceRequestContentsWithoutPayerSigningPubkey { metadata: self.payer.0.as_bytes(), }; - let offer = self.offer.as_tlv_stream(); + let (offer, experimental_offer) = self.offer.as_tlv_stream(); let features = { if self.features == InvoiceRequestFeatures::empty() { None } @@ -1021,7 +1088,12 @@ impl InvoiceRequestContentsWithoutPayerSigningPubkey { paths: None, }; - (payer, offer, invoice_request) + let experimental_invoice_request = ExperimentalInvoiceRequestTlvStreamRef { + #[cfg(test)] + experimental_bar: self.experimental_bar, + }; + + (payer, offer, invoice_request, experimental_offer, experimental_invoice_request) } } @@ -1061,7 +1133,7 @@ pub(super) const INVOICE_REQUEST_PAYER_ID_TYPE: u64 = 88; // This TLV stream is used for both InvoiceRequest and Refund, but not all TLV records are valid for // InvoiceRequest as noted below. -tlv_stream!(InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef, INVOICE_REQUEST_TYPES, { +tlv_stream!(InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef<'a>, INVOICE_REQUEST_TYPES, { (80, chain: ChainHash), (82, amount: (u64, HighZeroBytesDroppedBigSize)), (84, features: (InvoiceRequestFeatures, WithoutLength)), @@ -1072,14 +1144,36 @@ tlv_stream!(InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef, INVOICE_REQUEST (90, paths: (Vec, WithoutLength)), }); -type FullInvoiceRequestTlvStream = - (PayerTlvStream, OfferTlvStream, InvoiceRequestTlvStream, SignatureTlvStream); +/// Valid type range for experimental invoice_request TLV records. +pub(super) const EXPERIMENTAL_INVOICE_REQUEST_TYPES: core::ops::Range = + 2_000_000_000..3_000_000_000; + +#[cfg(not(test))] +tlv_stream!( + ExperimentalInvoiceRequestTlvStream, ExperimentalInvoiceRequestTlvStreamRef, + EXPERIMENTAL_INVOICE_REQUEST_TYPES, {} +); + +#[cfg(test)] +tlv_stream!( + ExperimentalInvoiceRequestTlvStream, ExperimentalInvoiceRequestTlvStreamRef, + EXPERIMENTAL_INVOICE_REQUEST_TYPES, { + (2_999_999_999, experimental_bar: (u64, HighZeroBytesDroppedBigSize)), + } +); + +type FullInvoiceRequestTlvStream = ( + PayerTlvStream, OfferTlvStream, InvoiceRequestTlvStream, SignatureTlvStream, + ExperimentalOfferTlvStream, ExperimentalInvoiceRequestTlvStream, +); type FullInvoiceRequestTlvStreamRef<'a> = ( PayerTlvStreamRef<'a>, OfferTlvStreamRef<'a>, InvoiceRequestTlvStreamRef<'a>, SignatureTlvStreamRef<'a>, + ExperimentalOfferTlvStreamRef, + ExperimentalInvoiceRequestTlvStreamRef, ); impl CursorReadable for FullInvoiceRequestTlvStream { @@ -1088,17 +1182,29 @@ impl CursorReadable for FullInvoiceRequestTlvStream { let offer = CursorReadable::read(r)?; let invoice_request = CursorReadable::read(r)?; let signature = CursorReadable::read(r)?; + let experimental_offer = CursorReadable::read(r)?; + let experimental_invoice_request = CursorReadable::read(r)?; - Ok((payer, offer, invoice_request, signature)) + Ok( + ( + payer, offer, invoice_request, signature, experimental_offer, + experimental_invoice_request, + ) + ) } } -type PartialInvoiceRequestTlvStream = (PayerTlvStream, OfferTlvStream, InvoiceRequestTlvStream); +type PartialInvoiceRequestTlvStream = ( + PayerTlvStream, OfferTlvStream, InvoiceRequestTlvStream, ExperimentalOfferTlvStream, + ExperimentalInvoiceRequestTlvStream, +); type PartialInvoiceRequestTlvStreamRef<'a> = ( PayerTlvStreamRef<'a>, OfferTlvStreamRef<'a>, InvoiceRequestTlvStreamRef<'a>, + ExperimentalOfferTlvStreamRef, + ExperimentalInvoiceRequestTlvStreamRef, ); impl TryFrom> for UnsignedInvoiceRequest { @@ -1106,17 +1212,18 @@ impl TryFrom> for UnsignedInvoiceRequest { fn try_from(bytes: Vec) -> Result { let invoice_request = ParsedMessage::::try_from(bytes)?; - let ParsedMessage { bytes, tlv_stream } = invoice_request; - let ( - payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, - ) = tlv_stream; - let contents = InvoiceRequestContents::try_from( - (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream) - )?; + let ParsedMessage { mut bytes, tlv_stream } = invoice_request; + let contents = InvoiceRequestContents::try_from(tlv_stream)?; 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 }) } } @@ -1129,9 +1236,14 @@ impl TryFrom> for InvoiceRequest { let ( payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, SignatureTlvStream { signature }, + experimental_offer_tlv_stream, + experimental_invoice_request_tlv_stream, ) = tlv_stream; let contents = InvoiceRequestContents::try_from( - (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream) + ( + payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, + experimental_offer_tlv_stream, experimental_invoice_request_tlv_stream, + ) )?; let signature = match signature { @@ -1155,13 +1267,18 @@ impl TryFrom for InvoiceRequestContents { InvoiceRequestTlvStream { chain, amount, features, quantity, payer_id, payer_note, paths, }, + experimental_offer_tlv_stream, + ExperimentalInvoiceRequestTlvStream { + #[cfg(test)] + experimental_bar, + }, ) = tlv_stream; let payer = match metadata { None => return Err(Bolt12SemanticError::MissingPayerMetadata), Some(metadata) => PayerContents(Metadata::Bytes(metadata)), }; - let offer = OfferContents::try_from(offer_tlv_stream)?; + let offer = OfferContents::try_from((offer_tlv_stream, experimental_offer_tlv_stream))?; if !offer.supports_chain(chain.unwrap_or_else(|| offer.implied_chain())) { return Err(Bolt12SemanticError::UnsupportedChain); @@ -1188,6 +1305,8 @@ impl TryFrom for InvoiceRequestContents { Ok(InvoiceRequestContents { inner: InvoiceRequestContentsWithoutPayerSigningPubkey { payer, offer, chain, amount_msats: amount, features, quantity, payer_note, + #[cfg(test)] + experimental_bar, }, payer_signing_pubkey, }) @@ -1242,7 +1361,7 @@ impl Readable for InvoiceRequestFields { #[cfg(test)] mod tests { - use super::{InvoiceRequest, InvoiceRequestFields, InvoiceRequestTlvStreamRef, PAYER_NOTE_LIMIT, SIGNATURE_TAG, UnsignedInvoiceRequest}; + use super::{EXPERIMENTAL_INVOICE_REQUEST_TYPES, ExperimentalInvoiceRequestTlvStreamRef, INVOICE_REQUEST_TYPES, InvoiceRequest, InvoiceRequestFields, InvoiceRequestTlvStreamRef, PAYER_NOTE_LIMIT, SIGNATURE_TAG, UnsignedInvoiceRequest}; use bitcoin::constants::ChainHash; use bitcoin::network::Network; @@ -1256,9 +1375,9 @@ mod tests { use crate::ln::inbound_payment::ExpandedKey; use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT}; use crate::offers::invoice::{Bolt12Invoice, SIGNATURE_TAG as INVOICE_SIGNATURE_TAG}; - use crate::offers::merkle::{SignError, SignatureTlvStreamRef, TaggedHash, self}; + use crate::offers::merkle::{SignError, SignatureTlvStreamRef, TaggedHash, TlvStream, self}; use crate::offers::nonce::Nonce; - use crate::offers::offer::{Amount, OfferTlvStreamRef, Quantity}; + use crate::offers::offer::{Amount, ExperimentalOfferTlvStreamRef, OfferTlvStreamRef, Quantity}; #[cfg(not(c_bindings))] use { crate::offers::offer::OfferBuilder, @@ -1367,6 +1486,12 @@ mod tests { paths: None, }, SignatureTlvStreamRef { signature: Some(&invoice_request.signature()) }, + ExperimentalOfferTlvStreamRef { + experimental_foo: None, + }, + ExperimentalInvoiceRequestTlvStreamRef { + experimental_bar: None, + }, ), ); @@ -1414,16 +1539,19 @@ mod tests { let offer = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) + .experimental_foo(42) .build().unwrap(); let invoice_request = offer .request_invoice_deriving_metadata(signing_pubkey, &expanded_key, nonce, payment_id) .unwrap() + .experimental_bar(42) .build().unwrap() .sign(payer_sign).unwrap(); assert_eq!(invoice_request.payer_signing_pubkey(), payer_pubkey()); let invoice = invoice_request.respond_with_no_std(payment_paths(), payment_hash(), now()) .unwrap() + .experimental_baz(42) .build().unwrap() .sign(recipient_sign).unwrap(); match invoice.verify_using_metadata(&expanded_key, &secp_ctx) { @@ -1437,22 +1565,29 @@ mod tests { // Fails verification with altered fields let ( payer_tlv_stream, offer_tlv_stream, mut invoice_request_tlv_stream, - mut invoice_tlv_stream, mut signature_tlv_stream + mut invoice_tlv_stream, mut signature_tlv_stream, experimental_offer_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); let tlv_stream = (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.write(&mut bytes).unwrap(); + (&tlv_stream, &experimental_tlv_stream).write(&mut bytes).unwrap(); let message = TaggedHash::from_valid_tlv_stream_bytes(INVOICE_SIGNATURE_TAG, &bytes); let signature = merkle::sign_message(recipient_sign, &message, recipient_pubkey()).unwrap(); signature_tlv_stream.signature = Some(&signature); - let mut encoded_invoice = bytes; - signature_tlv_stream.write(&mut encoded_invoice).unwrap(); + let mut encoded_invoice = Vec::new(); + (tlv_stream, signature_tlv_stream, experimental_tlv_stream) + .write(&mut encoded_invoice) + .unwrap(); let invoice = Bolt12Invoice::try_from(encoded_invoice).unwrap(); assert!(invoice.verify_using_metadata(&expanded_key, &secp_ctx).is_err()); @@ -1460,22 +1595,29 @@ mod tests { // Fails verification with altered metadata let ( mut payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream, - mut signature_tlv_stream + mut signature_tlv_stream, experimental_offer_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); let tlv_stream = (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.write(&mut bytes).unwrap(); + (&tlv_stream, &experimental_tlv_stream).write(&mut bytes).unwrap(); let message = TaggedHash::from_valid_tlv_stream_bytes(INVOICE_SIGNATURE_TAG, &bytes); let signature = merkle::sign_message(recipient_sign, &message, recipient_pubkey()).unwrap(); signature_tlv_stream.signature = Some(&signature); - let mut encoded_invoice = bytes; - signature_tlv_stream.write(&mut encoded_invoice).unwrap(); + let mut encoded_invoice = Vec::new(); + (tlv_stream, signature_tlv_stream, experimental_tlv_stream) + .write(&mut encoded_invoice) + .unwrap(); let invoice = Bolt12Invoice::try_from(encoded_invoice).unwrap(); assert!(invoice.verify_using_metadata(&expanded_key, &secp_ctx).is_err()); @@ -1491,15 +1633,18 @@ mod tests { let offer = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) + .experimental_foo(42) .build().unwrap(); let invoice_request = offer .request_invoice_deriving_signing_pubkey(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() + .experimental_bar(42) .build_and_sign() .unwrap(); let invoice = invoice_request.respond_with_no_std(payment_paths(), payment_hash(), now()) .unwrap() + .experimental_baz(42) .build().unwrap() .sign(recipient_sign).unwrap(); assert!(invoice.verify_using_metadata(&expanded_key, &secp_ctx).is_err()); @@ -1510,22 +1655,29 @@ mod tests { // Fails verification with altered fields let ( payer_tlv_stream, offer_tlv_stream, mut invoice_request_tlv_stream, - mut invoice_tlv_stream, mut signature_tlv_stream + mut invoice_tlv_stream, mut signature_tlv_stream, experimental_offer_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); let tlv_stream = (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.write(&mut bytes).unwrap(); + (&tlv_stream, &experimental_tlv_stream).write(&mut bytes).unwrap(); let message = TaggedHash::from_valid_tlv_stream_bytes(INVOICE_SIGNATURE_TAG, &bytes); let signature = merkle::sign_message(recipient_sign, &message, recipient_pubkey()).unwrap(); signature_tlv_stream.signature = Some(&signature); - let mut encoded_invoice = bytes; - signature_tlv_stream.write(&mut encoded_invoice).unwrap(); + let mut encoded_invoice = Vec::new(); + (tlv_stream, signature_tlv_stream, experimental_tlv_stream) + .write(&mut encoded_invoice) + .unwrap(); let invoice = Bolt12Invoice::try_from(encoded_invoice).unwrap(); assert!( @@ -1535,22 +1687,29 @@ mod tests { // Fails verification with altered payer id let ( payer_tlv_stream, offer_tlv_stream, mut invoice_request_tlv_stream, invoice_tlv_stream, - mut signature_tlv_stream + mut signature_tlv_stream, experimental_offer_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); let tlv_stream = (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.write(&mut bytes).unwrap(); + (&tlv_stream, &experimental_tlv_stream).write(&mut bytes).unwrap(); let message = TaggedHash::from_valid_tlv_stream_bytes(INVOICE_SIGNATURE_TAG, &bytes); let signature = merkle::sign_message(recipient_sign, &message, recipient_pubkey()).unwrap(); signature_tlv_stream.signature = Some(&signature); - let mut encoded_invoice = bytes; - signature_tlv_stream.write(&mut encoded_invoice).unwrap(); + let mut encoded_invoice = Vec::new(); + (tlv_stream, signature_tlv_stream, experimental_tlv_stream) + .write(&mut encoded_invoice) + .unwrap(); let invoice = Bolt12Invoice::try_from(encoded_invoice).unwrap(); assert!( @@ -1570,7 +1729,7 @@ mod tests { .chain(Network::Bitcoin).unwrap() .build().unwrap() .sign(payer_sign).unwrap(); - let (_, _, tlv_stream, _) = invoice_request.as_tlv_stream(); + let (_, _, tlv_stream, _, _, _) = invoice_request.as_tlv_stream(); assert_eq!(invoice_request.chain(), mainnet); assert_eq!(tlv_stream.chain, None); @@ -1582,7 +1741,7 @@ mod tests { .chain(Network::Testnet).unwrap() .build().unwrap() .sign(payer_sign).unwrap(); - let (_, _, tlv_stream, _) = invoice_request.as_tlv_stream(); + let (_, _, tlv_stream, _, _, _) = invoice_request.as_tlv_stream(); assert_eq!(invoice_request.chain(), testnet); assert_eq!(tlv_stream.chain, Some(&testnet)); @@ -1595,7 +1754,7 @@ mod tests { .chain(Network::Bitcoin).unwrap() .build().unwrap() .sign(payer_sign).unwrap(); - let (_, _, tlv_stream, _) = invoice_request.as_tlv_stream(); + let (_, _, tlv_stream, _, _, _) = invoice_request.as_tlv_stream(); assert_eq!(invoice_request.chain(), mainnet); assert_eq!(tlv_stream.chain, None); @@ -1609,7 +1768,7 @@ mod tests { .chain(Network::Testnet).unwrap() .build().unwrap() .sign(payer_sign).unwrap(); - let (_, _, tlv_stream, _) = invoice_request.as_tlv_stream(); + let (_, _, tlv_stream, _, _, _) = invoice_request.as_tlv_stream(); assert_eq!(invoice_request.chain(), testnet); assert_eq!(tlv_stream.chain, Some(&testnet)); @@ -1645,7 +1804,7 @@ mod tests { .amount_msats(1000).unwrap() .build().unwrap() .sign(payer_sign).unwrap(); - let (_, _, tlv_stream, _) = invoice_request.as_tlv_stream(); + let (_, _, tlv_stream, _, _, _) = invoice_request.as_tlv_stream(); assert_eq!(invoice_request.amount_msats(), Some(1000)); assert_eq!(tlv_stream.amount, Some(1000)); @@ -1657,7 +1816,7 @@ mod tests { .amount_msats(1000).unwrap() .build().unwrap() .sign(payer_sign).unwrap(); - let (_, _, tlv_stream, _) = invoice_request.as_tlv_stream(); + let (_, _, tlv_stream, _, _, _) = invoice_request.as_tlv_stream(); assert_eq!(invoice_request.amount_msats(), Some(1000)); assert_eq!(tlv_stream.amount, Some(1000)); @@ -1668,7 +1827,7 @@ mod tests { .amount_msats(1001).unwrap() .build().unwrap() .sign(payer_sign).unwrap(); - let (_, _, tlv_stream, _) = invoice_request.as_tlv_stream(); + let (_, _, tlv_stream, _, _, _) = invoice_request.as_tlv_stream(); assert_eq!(invoice_request.amount_msats(), Some(1001)); assert_eq!(tlv_stream.amount, Some(1001)); @@ -1748,7 +1907,7 @@ mod tests { .features_unchecked(InvoiceRequestFeatures::unknown()) .build().unwrap() .sign(payer_sign).unwrap(); - let (_, _, tlv_stream, _) = invoice_request.as_tlv_stream(); + let (_, _, tlv_stream, _, _, _) = invoice_request.as_tlv_stream(); assert_eq!(invoice_request.invoice_request_features(), &InvoiceRequestFeatures::unknown()); assert_eq!(tlv_stream.features, Some(&InvoiceRequestFeatures::unknown())); @@ -1760,7 +1919,7 @@ mod tests { .features_unchecked(InvoiceRequestFeatures::empty()) .build().unwrap() .sign(payer_sign).unwrap(); - let (_, _, tlv_stream, _) = invoice_request.as_tlv_stream(); + let (_, _, tlv_stream, _, _, _) = invoice_request.as_tlv_stream(); assert_eq!(invoice_request.invoice_request_features(), &InvoiceRequestFeatures::empty()); assert_eq!(tlv_stream.features, None); } @@ -1777,7 +1936,7 @@ mod tests { .request_invoice(vec![1; 32], payer_pubkey()).unwrap() .build().unwrap() .sign(payer_sign).unwrap(); - let (_, _, tlv_stream, _) = invoice_request.as_tlv_stream(); + let (_, _, tlv_stream, _, _, _) = invoice_request.as_tlv_stream(); assert_eq!(invoice_request.quantity(), None); assert_eq!(tlv_stream.quantity, None); @@ -1802,7 +1961,7 @@ mod tests { .quantity(10).unwrap() .build().unwrap() .sign(payer_sign).unwrap(); - let (_, _, tlv_stream, _) = invoice_request.as_tlv_stream(); + let (_, _, tlv_stream, _, _, _) = invoice_request.as_tlv_stream(); assert_eq!(invoice_request.amount_msats(), Some(10_000)); assert_eq!(tlv_stream.amount, Some(10_000)); @@ -1827,7 +1986,7 @@ mod tests { .quantity(2).unwrap() .build().unwrap() .sign(payer_sign).unwrap(); - let (_, _, tlv_stream, _) = invoice_request.as_tlv_stream(); + let (_, _, tlv_stream, _, _, _) = invoice_request.as_tlv_stream(); assert_eq!(invoice_request.amount_msats(), Some(2_000)); assert_eq!(tlv_stream.amount, Some(2_000)); @@ -1863,7 +2022,7 @@ mod tests { .payer_note("bar".into()) .build().unwrap() .sign(payer_sign).unwrap(); - let (_, _, tlv_stream, _) = invoice_request.as_tlv_stream(); + let (_, _, tlv_stream, _, _, _) = invoice_request.as_tlv_stream(); assert_eq!(invoice_request.payer_note(), Some(PrintableString("bar"))); assert_eq!(tlv_stream.payer_note, Some(&String::from("bar"))); @@ -1875,7 +2034,7 @@ mod tests { .payer_note("baz".into()) .build().unwrap() .sign(payer_sign).unwrap(); - let (_, _, tlv_stream, _) = invoice_request.as_tlv_stream(); + let (_, _, tlv_stream, _, _, _) = invoice_request.as_tlv_stream(); assert_eq!(invoice_request.payer_note(), Some(PrintableString("baz"))); assert_eq!(tlv_stream.payer_note, Some(&String::from("baz"))); } @@ -2294,9 +2453,196 @@ mod tests { } #[test] - fn fails_parsing_invoice_request_with_extra_tlv_records() { + fn parses_invoice_request_with_unknown_tlv_records() { + const UNKNOWN_ODD_TYPE: u64 = INVOICE_REQUEST_TYPES.end - 1; + assert!(UNKNOWN_ODD_TYPE % 2 == 1); + let secp_ctx = Secp256k1::new(); let keys = Keypair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap()); + let mut unsigned_invoice_request = OfferBuilder::new(keys.public_key()) + .amount_msats(1000) + .build().unwrap() + .request_invoice(vec![1; 32], keys.public_key()).unwrap() + .build().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); + + let invoice_request = unsigned_invoice_request + .sign(|message: &UnsignedInvoiceRequest| + Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys)) + ) + .unwrap(); + + let mut encoded_invoice_request = Vec::new(); + invoice_request.write(&mut encoded_invoice_request).unwrap(); + + match InvoiceRequest::try_from(encoded_invoice_request.clone()) { + Ok(invoice_request) => assert_eq!(invoice_request.bytes, encoded_invoice_request), + Err(e) => panic!("error parsing invoice_request: {:?}", e), + } + + const UNKNOWN_EVEN_TYPE: u64 = INVOICE_REQUEST_TYPES.end - 2; + assert!(UNKNOWN_EVEN_TYPE % 2 == 0); + + let mut unsigned_invoice_request = OfferBuilder::new(keys.public_key()) + .amount_msats(1000) + .build().unwrap() + .request_invoice(vec![1; 32], keys.public_key()).unwrap() + .build().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); + + let invoice_request = unsigned_invoice_request + .sign(|message: &UnsignedInvoiceRequest| + Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys)) + ) + .unwrap(); + + let mut encoded_invoice_request = Vec::new(); + invoice_request.write(&mut encoded_invoice_request).unwrap(); + + match InvoiceRequest::try_from(encoded_invoice_request) { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, Bolt12ParseError::Decode(DecodeError::UnknownRequiredFeature)), + } + } + + #[test] + fn parses_invoice_request_with_experimental_tlv_records() { + const UNKNOWN_ODD_TYPE: u64 = EXPERIMENTAL_INVOICE_REQUEST_TYPES.start + 1; + assert!(UNKNOWN_ODD_TYPE % 2 == 1); + + let secp_ctx = Secp256k1::new(); + let keys = Keypair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap()); + let mut unsigned_invoice_request = OfferBuilder::new(keys.public_key()) + .amount_msats(1000) + .build().unwrap() + .request_invoice(vec![1; 32], keys.public_key()).unwrap() + .build().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.experimental_bytes.extend_from_slice(&unknown_bytes); + + let tlv_stream = TlvStream::new(&unsigned_invoice_request.bytes) + .chain(TlvStream::new(&unsigned_invoice_request.experimental_bytes)); + unsigned_invoice_request.tagged_hash = + TaggedHash::from_tlv_stream(SIGNATURE_TAG, tlv_stream); + + let invoice_request = unsigned_invoice_request + .sign(|message: &UnsignedInvoiceRequest| + Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys)) + ) + .unwrap(); + + let mut encoded_invoice_request = Vec::new(); + invoice_request.write(&mut encoded_invoice_request).unwrap(); + + match InvoiceRequest::try_from(encoded_invoice_request.clone()) { + Ok(invoice_request) => assert_eq!(invoice_request.bytes, encoded_invoice_request), + Err(e) => panic!("error parsing invoice_request: {:?}", e), + } + + const UNKNOWN_EVEN_TYPE: u64 = EXPERIMENTAL_INVOICE_REQUEST_TYPES.start; + assert!(UNKNOWN_EVEN_TYPE % 2 == 0); + + let mut unsigned_invoice_request = OfferBuilder::new(keys.public_key()) + .amount_msats(1000) + .build().unwrap() + .request_invoice(vec![1; 32], keys.public_key()).unwrap() + .build().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.experimental_bytes.extend_from_slice(&unknown_bytes); + + let tlv_stream = TlvStream::new(&unsigned_invoice_request.bytes) + .chain(TlvStream::new(&unsigned_invoice_request.experimental_bytes)); + unsigned_invoice_request.tagged_hash = + TaggedHash::from_tlv_stream(SIGNATURE_TAG, tlv_stream); + + let invoice_request = unsigned_invoice_request + .sign(|message: &UnsignedInvoiceRequest| + Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys)) + ) + .unwrap(); + + let mut encoded_invoice_request = Vec::new(); + invoice_request.write(&mut encoded_invoice_request).unwrap(); + + match InvoiceRequest::try_from(encoded_invoice_request) { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, Bolt12ParseError::Decode(DecodeError::UnknownRequiredFeature)), + } + + let invoice_request = OfferBuilder::new(keys.public_key()) + .amount_msats(1000) + .build().unwrap() + .request_invoice(vec![1; 32], keys.public_key()).unwrap() + .build().unwrap() + .sign(|message: &UnsignedInvoiceRequest| + Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys)) + ) + .unwrap(); + + let mut encoded_invoice_request = Vec::new(); + invoice_request.write(&mut encoded_invoice_request).unwrap(); + + BigSize(UNKNOWN_ODD_TYPE).write(&mut encoded_invoice_request).unwrap(); + BigSize(32).write(&mut encoded_invoice_request).unwrap(); + [42u8; 32].write(&mut encoded_invoice_request).unwrap(); + + match InvoiceRequest::try_from(encoded_invoice_request) { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, Bolt12ParseError::InvalidSignature(secp256k1::Error::IncorrectSignature)), + } + } + + #[test] + fn fails_parsing_invoice_request_with_out_of_range_tlv_records() { + let secp_ctx = Secp256k1::new(); + let keys = Keypair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap()); + let invoice_request = OfferBuilder::new(keys.public_key()) .amount_msats(1000) .build().unwrap() @@ -2317,6 +2663,17 @@ mod tests { Ok(_) => panic!("expected error"), Err(e) => assert_eq!(e, Bolt12ParseError::Decode(DecodeError::InvalidValue)), } + + let mut encoded_invoice_request = Vec::new(); + invoice_request.write(&mut encoded_invoice_request).unwrap(); + BigSize(EXPERIMENTAL_INVOICE_REQUEST_TYPES.end).write(&mut encoded_invoice_request).unwrap(); + BigSize(32).write(&mut encoded_invoice_request).unwrap(); + [42u8; 32].write(&mut encoded_invoice_request).unwrap(); + + match InvoiceRequest::try_from(encoded_invoice_request) { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, Bolt12ParseError::Decode(DecodeError::InvalidValue)), + } } #[test] diff --git a/lightning/src/offers/merkle.rs b/lightning/src/offers/merkle.rs index e2fed2e800b..3497881faf9 100644 --- a/lightning/src/offers/merkle.rs +++ b/lightning/src/offers/merkle.rs @@ -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 = 240..=1000; +pub(super) const SIGNATURE_TYPES: core::ops::RangeInclusive = 240..=1000; -tlv_stream!(SignatureTlvStream, SignatureTlvStreamRef, SIGNATURE_TYPES, { +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>>(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,21 +245,16 @@ 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> - ) -> impl core::iter::Iterator> { - tlv_stream.filter(|record| !SIGNATURE_TYPES.contains(&record.r#type)) - } } /// A slice into a [`TlvStream`] for a record. -#[derive(Eq, PartialEq)] pub(super) struct TlvRecord<'a> { pub(super) r#type: u64, 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> { @@ -277,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(&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; @@ -412,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!( diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs index 0880e369eed..4efd0096cfe 100644 --- a/lightning/src/offers/offer.rs +++ b/lightning/src/offers/offer.rs @@ -91,11 +91,11 @@ use crate::ln::channelmanager::PaymentId; use crate::types::features::OfferFeatures; use crate::ln::inbound_payment::{ExpandedKey, IV_LEN}; use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT}; -use crate::offers::merkle::{TaggedHash, TlvStream}; +use crate::offers::merkle::{TaggedHash, TlvRecord, TlvStream}; use crate::offers::nonce::Nonce; use crate::offers::parse::{Bech32Encode, Bolt12ParseError, Bolt12SemanticError, ParsedMessage}; use crate::offers::signer::{Metadata, MetadataMaterial, self}; -use crate::util::ser::{HighZeroBytesDroppedBigSize, Readable, WithoutLength, Writeable, Writer}; +use crate::util::ser::{CursorReadable, HighZeroBytesDroppedBigSize, Readable, WithoutLength, Writeable, Writer}; use crate::util::string::PrintableString; #[cfg(not(c_bindings))] @@ -130,7 +130,7 @@ impl OfferId { } fn from_valid_invreq_tlv_stream(bytes: &[u8]) -> Self { - let tlv_stream = TlvStream::new(bytes).range(OFFER_TYPES); + let tlv_stream = Offer::tlv_stream_iter(bytes); let tagged_hash = TaggedHash::from_tlv_stream(Self::ID_TAG, tlv_stream); Self(tagged_hash.to_bytes()) } @@ -239,6 +239,8 @@ macro_rules! offer_explicit_metadata_builder_methods { ( chains: None, metadata: None, amount: None, description: None, features: OfferFeatures::empty(), absolute_expiry: None, issuer: None, paths: None, supported_quantity: Quantity::One, issuer_signing_pubkey: Some(signing_pubkey), + #[cfg(test)] + experimental_foo: None, }, metadata_strategy: core::marker::PhantomData, secp_ctx: None, @@ -280,6 +282,8 @@ macro_rules! offer_derived_metadata_builder_methods { ($secp_context: ty) => { chains: None, metadata: Some(metadata), amount: None, description: None, features: OfferFeatures::empty(), absolute_expiry: None, issuer: None, paths: None, supported_quantity: Quantity::One, issuer_signing_pubkey: Some(node_id), + #[cfg(test)] + experimental_foo: None, }, metadata_strategy: core::marker::PhantomData, secp_ctx: Some(secp_ctx), @@ -414,10 +418,10 @@ macro_rules! offer_builder_methods { ( }; let mut tlv_stream = $self.offer.as_tlv_stream(); - debug_assert_eq!(tlv_stream.metadata, None); - tlv_stream.metadata = None; + debug_assert_eq!(tlv_stream.0.metadata, None); + tlv_stream.0.metadata = None; if metadata.derives_recipient_keys() { - tlv_stream.issuer_id = None; + tlv_stream.0.issuer_id = None; } // Either replace the signing pubkey with the derived pubkey or include the metadata @@ -478,6 +482,12 @@ macro_rules! offer_builder_test_methods { ( $return_value } + #[cfg_attr(c_bindings, allow(dead_code))] + pub(super) fn experimental_foo($($self_mut)* $self: $self_type, experimental_foo: u64) -> $return_type { + $self.offer.experimental_foo = Some(experimental_foo); + $return_value + } + #[cfg_attr(c_bindings, allow(dead_code))] pub(super) fn build_unchecked($self: $self_type) -> Offer { $self.build_without_checks() @@ -585,6 +595,8 @@ pub(super) struct OfferContents { paths: Option>, supported_quantity: Quantity, issuer_signing_pubkey: Option, + #[cfg(test)] + experimental_foo: Option, } macro_rules! offer_accessors { ($self: ident, $contents: expr) => { @@ -701,6 +713,13 @@ impl Offer { self.contents.expects_quantity() } + pub(super) fn tlv_stream_iter<'a>( + bytes: &'a [u8] + ) -> impl core::iter::Iterator> { + TlvStream::new(bytes).range(OFFER_TYPES) + .chain(TlvStream::new(bytes).range(EXPERIMENTAL_OFFER_TYPES)) + } + #[cfg(async_payments)] pub(super) fn verify( &self, nonce: Nonce, key: &ExpandedKey, secp_ctx: &Secp256k1 @@ -803,7 +822,7 @@ impl Offer { #[cfg(test)] impl Offer { - pub(super) fn as_tlv_stream(&self) -> OfferTlvStreamRef { + pub(super) fn as_tlv_stream(&self) -> FullOfferTlvStreamRef { self.contents.as_tlv_stream() } } @@ -971,7 +990,9 @@ impl OfferContents { OFFER_ISSUER_ID_TYPE => !metadata.derives_recipient_keys(), _ => true, } - }); + }) + .chain(TlvStream::new(bytes).range(EXPERIMENTAL_OFFER_TYPES)); + let signing_pubkey = match self.issuer_signing_pubkey() { Some(signing_pubkey) => signing_pubkey, None => return Err(()), @@ -988,7 +1009,7 @@ impl OfferContents { } } - pub(super) fn as_tlv_stream(&self) -> OfferTlvStreamRef { + pub(super) fn as_tlv_stream(&self) -> FullOfferTlvStreamRef { let (currency, amount) = match &self.amount { None => (None, None), Some(Amount::Bitcoin { amount_msats }) => (None, Some(*amount_msats)), @@ -1001,7 +1022,7 @@ impl OfferContents { if self.features == OfferFeatures::empty() { None } else { Some(&self.features) } }; - OfferTlvStreamRef { + let offer = OfferTlvStreamRef { chains: self.chains.as_ref(), metadata: self.metadata(), currency, @@ -1013,7 +1034,14 @@ impl OfferContents { issuer: self.issuer.as_ref(), quantity_max: self.supported_quantity.to_tlv_record(), issuer_id: self.issuer_signing_pubkey.as_ref(), - } + }; + + let experimental_offer = ExperimentalOfferTlvStreamRef { + #[cfg(test)] + experimental_foo: self.experimental_foo, + }; + + (offer, experimental_offer) } } @@ -1091,7 +1119,7 @@ const OFFER_METADATA_TYPE: u64 = 4; /// TLV record type for [`Offer::issuer_signing_pubkey`]. const OFFER_ISSUER_ID_TYPE: u64 = 22; -tlv_stream!(OfferTlvStream, OfferTlvStreamRef, OFFER_TYPES, { +tlv_stream!(OfferTlvStream, OfferTlvStreamRef<'a>, OFFER_TYPES, { (2, chains: (Vec, WithoutLength)), (OFFER_METADATA_TYPE, metadata: (Vec, WithoutLength)), (6, currency: CurrencyCode), @@ -1105,6 +1133,31 @@ tlv_stream!(OfferTlvStream, OfferTlvStreamRef, 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 = 1_000_000_000..2_000_000_000; + +#[cfg(not(test))] +tlv_stream!(ExperimentalOfferTlvStream, ExperimentalOfferTlvStreamRef, EXPERIMENTAL_OFFER_TYPES, { +}); + +#[cfg(test)] +tlv_stream!(ExperimentalOfferTlvStream, ExperimentalOfferTlvStreamRef, EXPERIMENTAL_OFFER_TYPES, { + (1_999_999_999, experimental_foo: (u64, HighZeroBytesDroppedBigSize)), +}); + +type FullOfferTlvStream = (OfferTlvStream, ExperimentalOfferTlvStream); + +type FullOfferTlvStreamRef<'a> = (OfferTlvStreamRef<'a>, ExperimentalOfferTlvStreamRef); + +impl CursorReadable for FullOfferTlvStream { + fn read>(r: &mut io::Cursor) -> Result { + let offer = CursorReadable::read(r)?; + let experimental_offer = CursorReadable::read(r)?; + + Ok((offer, experimental_offer)) + } +} + impl Bech32Encode for Offer { const BECH32_HRP: &'static str = "lno"; } @@ -1121,7 +1174,7 @@ impl TryFrom> for Offer { type Error = Bolt12ParseError; fn try_from(bytes: Vec) -> Result { - let offer = ParsedMessage::::try_from(bytes)?; + let offer = ParsedMessage::::try_from(bytes)?; let ParsedMessage { bytes, tlv_stream } = offer; let contents = OfferContents::try_from(tlv_stream)?; let id = OfferId::from_valid_offer_tlv_stream(&bytes); @@ -1130,14 +1183,20 @@ impl TryFrom> for Offer { } } -impl TryFrom for OfferContents { +impl TryFrom for OfferContents { type Error = Bolt12SemanticError; - fn try_from(tlv_stream: OfferTlvStream) -> Result { - let OfferTlvStream { - chains, metadata, currency, amount, description, features, absolute_expiry, paths, - issuer, quantity_max, issuer_id, - } = tlv_stream; + fn try_from(tlv_stream: FullOfferTlvStream) -> Result { + let ( + OfferTlvStream { + chains, metadata, currency, amount, description, features, absolute_expiry, paths, + issuer, quantity_max, issuer_id, + }, + ExperimentalOfferTlvStream { + #[cfg(test)] + experimental_foo, + }, + ) = tlv_stream; let metadata = metadata.map(|metadata| Metadata::Bytes(metadata)); @@ -1175,6 +1234,8 @@ impl TryFrom for OfferContents { Ok(OfferContents { chains, metadata, amount, description, features, absolute_expiry, issuer, paths, supported_quantity, issuer_signing_pubkey, + #[cfg(test)] + experimental_foo, }) } } @@ -1187,7 +1248,7 @@ impl core::fmt::Display for Offer { #[cfg(test)] mod tests { - use super::{Amount, Offer, OfferTlvStreamRef, Quantity}; + use super::{Amount, EXPERIMENTAL_OFFER_TYPES, ExperimentalOfferTlvStreamRef, OFFER_TYPES, Offer, OfferTlvStreamRef, Quantity}; #[cfg(not(c_bindings))] use { super::OfferBuilder, @@ -1239,19 +1300,24 @@ mod tests { assert_eq!( offer.as_tlv_stream(), - OfferTlvStreamRef { - chains: None, - metadata: None, - currency: None, - amount: None, - description: None, - features: None, - absolute_expiry: None, - paths: None, - issuer: None, - quantity_max: None, - issuer_id: Some(&pubkey(42)), - }, + ( + OfferTlvStreamRef { + chains: None, + metadata: None, + currency: None, + amount: None, + description: None, + features: None, + absolute_expiry: None, + paths: None, + issuer: None, + quantity_max: None, + issuer_id: Some(&pubkey(42)), + }, + ExperimentalOfferTlvStreamRef { + experimental_foo: None, + }, + ), ); if let Err(e) = Offer::try_from(buffer) { @@ -1270,7 +1336,7 @@ mod tests { .unwrap(); assert!(offer.supports_chain(mainnet)); assert_eq!(offer.chains(), vec![mainnet]); - assert_eq!(offer.as_tlv_stream().chains, None); + assert_eq!(offer.as_tlv_stream().0.chains, None); let offer = OfferBuilder::new(pubkey(42)) .chain(Network::Testnet) @@ -1278,7 +1344,7 @@ mod tests { .unwrap(); assert!(offer.supports_chain(testnet)); assert_eq!(offer.chains(), vec![testnet]); - assert_eq!(offer.as_tlv_stream().chains, Some(&vec![testnet])); + assert_eq!(offer.as_tlv_stream().0.chains, Some(&vec![testnet])); let offer = OfferBuilder::new(pubkey(42)) .chain(Network::Testnet) @@ -1287,7 +1353,7 @@ mod tests { .unwrap(); assert!(offer.supports_chain(testnet)); assert_eq!(offer.chains(), vec![testnet]); - assert_eq!(offer.as_tlv_stream().chains, Some(&vec![testnet])); + assert_eq!(offer.as_tlv_stream().0.chains, Some(&vec![testnet])); let offer = OfferBuilder::new(pubkey(42)) .chain(Network::Bitcoin) @@ -1297,7 +1363,7 @@ mod tests { assert!(offer.supports_chain(mainnet)); assert!(offer.supports_chain(testnet)); assert_eq!(offer.chains(), vec![mainnet, testnet]); - assert_eq!(offer.as_tlv_stream().chains, Some(&vec![mainnet, testnet])); + assert_eq!(offer.as_tlv_stream().0.chains, Some(&vec![mainnet, testnet])); } #[test] @@ -1307,7 +1373,7 @@ mod tests { .build() .unwrap(); assert_eq!(offer.metadata(), Some(&vec![42; 32])); - assert_eq!(offer.as_tlv_stream().metadata, Some(&vec![42; 32])); + assert_eq!(offer.as_tlv_stream().0.metadata, Some(&vec![42; 32])); let offer = OfferBuilder::new(pubkey(42)) .metadata(vec![42; 32]).unwrap() @@ -1315,7 +1381,7 @@ mod tests { .build() .unwrap(); assert_eq!(offer.metadata(), Some(&vec![43; 32])); - assert_eq!(offer.as_tlv_stream().metadata, Some(&vec![43; 32])); + assert_eq!(offer.as_tlv_stream().0.metadata, Some(&vec![43; 32])); } #[test] @@ -1330,6 +1396,7 @@ mod tests { use super::OfferWithDerivedMetadataBuilder as OfferBuilder; let offer = OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, nonce, &secp_ctx) .amount_msats(1000) + .experimental_foo(42) .build().unwrap(); assert!(offer.metadata().is_some()); assert_eq!(offer.issuer_signing_pubkey(), Some(node_id)); @@ -1352,7 +1419,7 @@ mod tests { // Fails verification with altered offer field let mut tlv_stream = offer.as_tlv_stream(); - tlv_stream.amount = Some(100); + tlv_stream.0.amount = Some(100); let mut encoded_offer = Vec::new(); tlv_stream.write(&mut encoded_offer).unwrap(); @@ -1365,8 +1432,8 @@ mod tests { // Fails verification with altered metadata let mut tlv_stream = offer.as_tlv_stream(); - let metadata = tlv_stream.metadata.unwrap().iter().copied().rev().collect(); - tlv_stream.metadata = Some(&metadata); + let metadata = tlv_stream.0.metadata.unwrap().iter().copied().rev().collect(); + tlv_stream.0.metadata = Some(&metadata); let mut encoded_offer = Vec::new(); tlv_stream.write(&mut encoded_offer).unwrap(); @@ -1399,6 +1466,7 @@ mod tests { let offer = OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, nonce, &secp_ctx) .amount_msats(1000) .path(blinded_path) + .experimental_foo(42) .build().unwrap(); assert!(offer.metadata().is_none()); assert_ne!(offer.issuer_signing_pubkey(), Some(node_id)); @@ -1419,7 +1487,7 @@ mod tests { // Fails verification with altered offer field let mut tlv_stream = offer.as_tlv_stream(); - tlv_stream.amount = Some(100); + tlv_stream.0.amount = Some(100); let mut encoded_offer = Vec::new(); tlv_stream.write(&mut encoded_offer).unwrap(); @@ -1435,7 +1503,7 @@ mod tests { // Fails verification with altered signing pubkey let mut tlv_stream = offer.as_tlv_stream(); let issuer_id = pubkey(1); - tlv_stream.issuer_id = Some(&issuer_id); + tlv_stream.0.issuer_id = Some(&issuer_id); let mut encoded_offer = Vec::new(); tlv_stream.write(&mut encoded_offer).unwrap(); @@ -1460,8 +1528,8 @@ mod tests { .unwrap(); let tlv_stream = offer.as_tlv_stream(); assert_eq!(offer.amount(), Some(bitcoin_amount)); - assert_eq!(tlv_stream.amount, Some(1000)); - assert_eq!(tlv_stream.currency, None); + assert_eq!(tlv_stream.0.amount, Some(1000)); + assert_eq!(tlv_stream.0.currency, None); #[cfg(not(c_bindings))] let builder = OfferBuilder::new(pubkey(42)) @@ -1472,8 +1540,8 @@ mod tests { builder.amount(currency_amount.clone()); let tlv_stream = builder.offer.as_tlv_stream(); assert_eq!(builder.offer.amount, Some(currency_amount.clone())); - assert_eq!(tlv_stream.amount, Some(10)); - assert_eq!(tlv_stream.currency, Some(b"USD")); + assert_eq!(tlv_stream.0.amount, Some(10)); + assert_eq!(tlv_stream.0.currency, Some(b"USD")); match builder.build() { Ok(_) => panic!("expected error"), Err(e) => assert_eq!(e, Bolt12SemanticError::UnsupportedCurrency), @@ -1485,8 +1553,8 @@ mod tests { .build() .unwrap(); let tlv_stream = offer.as_tlv_stream(); - assert_eq!(tlv_stream.amount, Some(1000)); - assert_eq!(tlv_stream.currency, None); + assert_eq!(tlv_stream.0.amount, Some(1000)); + assert_eq!(tlv_stream.0.currency, None); let invalid_amount = Amount::Bitcoin { amount_msats: MAX_VALUE_MSAT + 1 }; match OfferBuilder::new(pubkey(42)).amount(invalid_amount).build() { @@ -1502,7 +1570,7 @@ mod tests { .build() .unwrap(); assert_eq!(offer.description(), Some(PrintableString("foo"))); - assert_eq!(offer.as_tlv_stream().description, Some(&String::from("foo"))); + assert_eq!(offer.as_tlv_stream().0.description, Some(&String::from("foo"))); let offer = OfferBuilder::new(pubkey(42)) .description("foo".into()) @@ -1510,14 +1578,14 @@ mod tests { .build() .unwrap(); assert_eq!(offer.description(), Some(PrintableString("bar"))); - assert_eq!(offer.as_tlv_stream().description, Some(&String::from("bar"))); + assert_eq!(offer.as_tlv_stream().0.description, Some(&String::from("bar"))); let offer = OfferBuilder::new(pubkey(42)) .amount_msats(1000) .build() .unwrap(); assert_eq!(offer.description(), Some(PrintableString(""))); - assert_eq!(offer.as_tlv_stream().description, Some(&String::from(""))); + assert_eq!(offer.as_tlv_stream().0.description, Some(&String::from(""))); } #[test] @@ -1527,7 +1595,7 @@ mod tests { .build() .unwrap(); assert_eq!(offer.offer_features(), &OfferFeatures::unknown()); - assert_eq!(offer.as_tlv_stream().features, Some(&OfferFeatures::unknown())); + assert_eq!(offer.as_tlv_stream().0.features, Some(&OfferFeatures::unknown())); let offer = OfferBuilder::new(pubkey(42)) .features_unchecked(OfferFeatures::unknown()) @@ -1535,7 +1603,7 @@ mod tests { .build() .unwrap(); assert_eq!(offer.offer_features(), &OfferFeatures::empty()); - assert_eq!(offer.as_tlv_stream().features, None); + assert_eq!(offer.as_tlv_stream().0.features, None); } #[test] @@ -1552,7 +1620,7 @@ mod tests { assert!(!offer.is_expired()); assert!(!offer.is_expired_no_std(now)); assert_eq!(offer.absolute_expiry(), Some(future_expiry)); - assert_eq!(offer.as_tlv_stream().absolute_expiry, Some(future_expiry.as_secs())); + assert_eq!(offer.as_tlv_stream().0.absolute_expiry, Some(future_expiry.as_secs())); let offer = OfferBuilder::new(pubkey(42)) .absolute_expiry(future_expiry) @@ -1563,7 +1631,7 @@ mod tests { assert!(offer.is_expired()); assert!(offer.is_expired_no_std(now)); assert_eq!(offer.absolute_expiry(), Some(past_expiry)); - assert_eq!(offer.as_tlv_stream().absolute_expiry, Some(past_expiry.as_secs())); + assert_eq!(offer.as_tlv_stream().0.absolute_expiry, Some(past_expiry.as_secs())); } #[test] @@ -1594,8 +1662,8 @@ mod tests { assert_eq!(offer.paths(), paths.as_slice()); assert_eq!(offer.issuer_signing_pubkey(), Some(pubkey(42))); assert_ne!(pubkey(42), pubkey(44)); - assert_eq!(tlv_stream.paths, Some(&paths)); - assert_eq!(tlv_stream.issuer_id, Some(&pubkey(42))); + assert_eq!(tlv_stream.0.paths, Some(&paths)); + assert_eq!(tlv_stream.0.issuer_id, Some(&pubkey(42))); } #[test] @@ -1605,7 +1673,7 @@ mod tests { .build() .unwrap(); assert_eq!(offer.issuer(), Some(PrintableString("foo"))); - assert_eq!(offer.as_tlv_stream().issuer, Some(&String::from("foo"))); + assert_eq!(offer.as_tlv_stream().0.issuer, Some(&String::from("foo"))); let offer = OfferBuilder::new(pubkey(42)) .issuer("foo".into()) @@ -1613,7 +1681,7 @@ mod tests { .build() .unwrap(); assert_eq!(offer.issuer(), Some(PrintableString("bar"))); - assert_eq!(offer.as_tlv_stream().issuer, Some(&String::from("bar"))); + assert_eq!(offer.as_tlv_stream().0.issuer, Some(&String::from("bar"))); } #[test] @@ -1628,7 +1696,7 @@ mod tests { let tlv_stream = offer.as_tlv_stream(); assert!(!offer.expects_quantity()); assert_eq!(offer.supported_quantity(), Quantity::One); - assert_eq!(tlv_stream.quantity_max, None); + assert_eq!(tlv_stream.0.quantity_max, None); let offer = OfferBuilder::new(pubkey(42)) .supported_quantity(Quantity::Unbounded) @@ -1637,7 +1705,7 @@ mod tests { let tlv_stream = offer.as_tlv_stream(); assert!(offer.expects_quantity()); assert_eq!(offer.supported_quantity(), Quantity::Unbounded); - assert_eq!(tlv_stream.quantity_max, Some(0)); + assert_eq!(tlv_stream.0.quantity_max, Some(0)); let offer = OfferBuilder::new(pubkey(42)) .supported_quantity(Quantity::Bounded(ten)) @@ -1646,7 +1714,7 @@ mod tests { let tlv_stream = offer.as_tlv_stream(); assert!(offer.expects_quantity()); assert_eq!(offer.supported_quantity(), Quantity::Bounded(ten)); - assert_eq!(tlv_stream.quantity_max, Some(10)); + assert_eq!(tlv_stream.0.quantity_max, Some(10)); let offer = OfferBuilder::new(pubkey(42)) .supported_quantity(Quantity::Bounded(one)) @@ -1655,7 +1723,7 @@ mod tests { let tlv_stream = offer.as_tlv_stream(); assert!(offer.expects_quantity()); assert_eq!(offer.supported_quantity(), Quantity::Bounded(one)); - assert_eq!(tlv_stream.quantity_max, Some(1)); + assert_eq!(tlv_stream.0.quantity_max, Some(1)); let offer = OfferBuilder::new(pubkey(42)) .supported_quantity(Quantity::Bounded(ten)) @@ -1665,7 +1733,7 @@ mod tests { let tlv_stream = offer.as_tlv_stream(); assert!(!offer.expects_quantity()); assert_eq!(offer.supported_quantity(), Quantity::One); - assert_eq!(tlv_stream.quantity_max, None); + assert_eq!(tlv_stream.0.quantity_max, None); } #[test] @@ -1703,8 +1771,8 @@ mod tests { } let mut tlv_stream = offer.as_tlv_stream(); - tlv_stream.amount = Some(1000); - tlv_stream.currency = Some(b"USD"); + tlv_stream.0.amount = Some(1000); + tlv_stream.0.currency = Some(b"USD"); let mut encoded_offer = Vec::new(); tlv_stream.write(&mut encoded_offer).unwrap(); @@ -1714,8 +1782,8 @@ mod tests { } let mut tlv_stream = offer.as_tlv_stream(); - tlv_stream.amount = None; - tlv_stream.currency = Some(b"USD"); + tlv_stream.0.amount = None; + tlv_stream.0.currency = Some(b"USD"); let mut encoded_offer = Vec::new(); tlv_stream.write(&mut encoded_offer).unwrap(); @@ -1726,8 +1794,8 @@ mod tests { } let mut tlv_stream = offer.as_tlv_stream(); - tlv_stream.amount = Some(MAX_VALUE_MSAT + 1); - tlv_stream.currency = None; + tlv_stream.0.amount = Some(MAX_VALUE_MSAT + 1); + tlv_stream.0.currency = None; let mut encoded_offer = Vec::new(); tlv_stream.write(&mut encoded_offer).unwrap(); @@ -1754,7 +1822,7 @@ mod tests { } let mut tlv_stream = offer.as_tlv_stream(); - tlv_stream.description = None; + tlv_stream.0.description = None; let mut encoded_offer = Vec::new(); tlv_stream.write(&mut encoded_offer).unwrap(); @@ -1860,7 +1928,7 @@ mod tests { } let mut tlv_stream = offer.as_tlv_stream(); - tlv_stream.issuer_id = None; + tlv_stream.0.issuer_id = None; let mut encoded_offer = Vec::new(); tlv_stream.write(&mut encoded_offer).unwrap(); @@ -1874,12 +1942,89 @@ mod tests { } #[test] - fn fails_parsing_offer_with_extra_tlv_records() { + fn parses_offer_with_unknown_tlv_records() { + const UNKNOWN_ODD_TYPE: u64 = OFFER_TYPES.end - 1; + assert!(UNKNOWN_ODD_TYPE % 2 == 1); + + let offer = OfferBuilder::new(pubkey(42)).build().unwrap(); + + let mut encoded_offer = Vec::new(); + offer.write(&mut encoded_offer).unwrap(); + BigSize(UNKNOWN_ODD_TYPE).write(&mut encoded_offer).unwrap(); + BigSize(32).write(&mut encoded_offer).unwrap(); + [42u8; 32].write(&mut encoded_offer).unwrap(); + + match Offer::try_from(encoded_offer.clone()) { + Ok(offer) => assert_eq!(offer.bytes, encoded_offer), + Err(e) => panic!("error parsing offer: {:?}", e), + } + + const UNKNOWN_EVEN_TYPE: u64 = OFFER_TYPES.end - 2; + assert!(UNKNOWN_EVEN_TYPE % 2 == 0); + + let offer = OfferBuilder::new(pubkey(42)).build().unwrap(); + + let mut encoded_offer = Vec::new(); + offer.write(&mut encoded_offer).unwrap(); + BigSize(UNKNOWN_EVEN_TYPE).write(&mut encoded_offer).unwrap(); + BigSize(32).write(&mut encoded_offer).unwrap(); + [42u8; 32].write(&mut encoded_offer).unwrap(); + + match Offer::try_from(encoded_offer) { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, Bolt12ParseError::Decode(DecodeError::UnknownRequiredFeature)), + } + } + + #[test] + fn parses_offer_with_experimental_tlv_records() { + let offer = OfferBuilder::new(pubkey(42)).build().unwrap(); + + let mut encoded_offer = Vec::new(); + offer.write(&mut encoded_offer).unwrap(); + BigSize(EXPERIMENTAL_OFFER_TYPES.start + 1).write(&mut encoded_offer).unwrap(); + BigSize(32).write(&mut encoded_offer).unwrap(); + [42u8; 32].write(&mut encoded_offer).unwrap(); + + match Offer::try_from(encoded_offer.clone()) { + Ok(offer) => assert_eq!(offer.bytes, encoded_offer), + Err(e) => panic!("error parsing offer: {:?}", e), + } + + let offer = OfferBuilder::new(pubkey(42)).build().unwrap(); + + let mut encoded_offer = Vec::new(); + offer.write(&mut encoded_offer).unwrap(); + BigSize(EXPERIMENTAL_OFFER_TYPES.start).write(&mut encoded_offer).unwrap(); + BigSize(32).write(&mut encoded_offer).unwrap(); + [42u8; 32].write(&mut encoded_offer).unwrap(); + + match Offer::try_from(encoded_offer) { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, Bolt12ParseError::Decode(DecodeError::UnknownRequiredFeature)), + } + } + + #[test] + fn fails_parsing_offer_with_out_of_range_tlv_records() { + let offer = OfferBuilder::new(pubkey(42)).build().unwrap(); + + let mut encoded_offer = Vec::new(); + offer.write(&mut encoded_offer).unwrap(); + BigSize(OFFER_TYPES.end).write(&mut encoded_offer).unwrap(); + BigSize(32).write(&mut encoded_offer).unwrap(); + [42u8; 32].write(&mut encoded_offer).unwrap(); + + match Offer::try_from(encoded_offer) { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, Bolt12ParseError::Decode(DecodeError::InvalidValue)), + } + let offer = OfferBuilder::new(pubkey(42)).build().unwrap(); let mut encoded_offer = Vec::new(); offer.write(&mut encoded_offer).unwrap(); - BigSize(80).write(&mut encoded_offer).unwrap(); + BigSize(EXPERIMENTAL_OFFER_TYPES.end).write(&mut encoded_offer).unwrap(); BigSize(32).write(&mut encoded_offer).unwrap(); [42u8; 32].write(&mut encoded_offer).unwrap(); @@ -1953,6 +2098,9 @@ mod bolt12_tests { // unknown odd field "lno1pgx9getnwss8vetrw3hhyuckyypwa3eyt44h6txtxquqh7lz5djge4afgfjn7k4rgrkuag0jsd5xvxfppf5x2mrvdamk7unvvs", + + // unknown odd experimental field + "lno1pgx9getnwss8vetrw3hhyuckyypwa3eyt44h6txtxquqh7lz5djge4afgfjn7k4rgrkuag0jsd5xvx078wdv5gg2dpjkcmr0wahhymry", ]; for encoded_offer in &offers { if let Err(e) = encoded_offer.parse::() { @@ -2095,6 +2243,18 @@ mod bolt12_tests { Err(Bolt12ParseError::Decode(DecodeError::InvalidValue)), ); + // Contains type > 1999999999 + assert_eq!( + "lno1pgz5znzfgdz3vggzqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgp06ae4jsq9qgr0u2xq4dh3kdevrf4zg6hx8a60jv0gxe0ptgyfc6xkryqqqqqqqq".parse::(), + Err(Bolt12ParseError::Decode(DecodeError::InvalidValue)), + ); + + // Contains unknown even type (1000000002) + assert_eq!( + "lno1pgz5znzfgdz3vggzqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgp06wu6egp9qgr0u2xq4dh3kdevrf4zg6hx8a60jv0gxe0ptgyfc6xkryqqqqqqqq".parse::(), + Err(Bolt12ParseError::Decode(DecodeError::InvalidValue)), + ); + // TODO: Resolved in spec https://github.com/lightning/bolts/pull/798/files#r1334851959 // Contains unknown feature 22 assert!( diff --git a/lightning/src/offers/payer.rs b/lightning/src/offers/payer.rs index 0ec5721dc38..696eac24044 100644 --- a/lightning/src/offers/payer.rs +++ b/lightning/src/offers/payer.rs @@ -30,6 +30,6 @@ pub(super) struct PayerContents(pub Metadata); /// [`Refund::payer_metadata`]: crate::offers::refund::Refund::payer_metadata pub(super) const PAYER_METADATA_TYPE: u64 = 0; -tlv_stream!(PayerTlvStream, PayerTlvStreamRef, 0..1, { +tlv_stream!(PayerTlvStream, PayerTlvStreamRef<'a>, 0..1, { (PAYER_METADATA_TYPE, metadata: (Vec, WithoutLength)), }); diff --git a/lightning/src/offers/refund.rs b/lightning/src/offers/refund.rs index 8d76534ff6d..b1f5b0520ca 100644 --- a/lightning/src/offers/refund.rs +++ b/lightning/src/offers/refund.rs @@ -98,9 +98,9 @@ use crate::ln::channelmanager::PaymentId; use crate::types::features::InvoiceRequestFeatures; use crate::ln::inbound_payment::{ExpandedKey, IV_LEN}; use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT}; -use crate::offers::invoice_request::{InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef}; +use crate::offers::invoice_request::{ExperimentalInvoiceRequestTlvStream, ExperimentalInvoiceRequestTlvStreamRef, InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef}; use crate::offers::nonce::Nonce; -use crate::offers::offer::{OfferTlvStream, OfferTlvStreamRef}; +use crate::offers::offer::{ExperimentalOfferTlvStream, ExperimentalOfferTlvStreamRef, OfferTlvStream, OfferTlvStreamRef}; use crate::offers::parse::{Bech32Encode, Bolt12ParseError, Bolt12SemanticError, ParsedMessage}; use crate::offers::payer::{PayerContents, PayerTlvStream, PayerTlvStreamRef}; use crate::offers::signer::{Metadata, MetadataMaterial, self}; @@ -176,6 +176,10 @@ macro_rules! refund_explicit_metadata_builder_methods { () => { payer: PayerContents(metadata), description: String::new(), absolute_expiry: None, issuer: None, chain: None, amount_msats, features: InvoiceRequestFeatures::empty(), quantity: None, payer_signing_pubkey: signing_pubkey, payer_note: None, paths: None, + #[cfg(test)] + experimental_foo: None, + #[cfg(test)] + experimental_bar: None, }, secp_ctx: None, }) @@ -218,6 +222,10 @@ macro_rules! refund_builder_methods { ( payer: PayerContents(metadata), description: String::new(), absolute_expiry: None, issuer: None, chain: None, amount_msats, features: InvoiceRequestFeatures::empty(), quantity: None, payer_signing_pubkey: node_id, payer_note: None, paths: None, + #[cfg(test)] + experimental_foo: None, + #[cfg(test)] + experimental_bar: None, }, secp_ctx: Some(secp_ctx), }) @@ -358,6 +366,18 @@ macro_rules! refund_builder_test_methods { ( $self.refund.features = features; $return_value } + + #[cfg_attr(c_bindings, allow(dead_code))] + pub(super) fn experimental_foo($($self_mut)* $self: $self_type, experimental_foo: u64) -> $return_type { + $self.refund.experimental_foo = Some(experimental_foo); + $return_value + } + + #[cfg_attr(c_bindings, allow(dead_code))] + pub(super) fn experimental_bar($($self_mut)* $self: $self_type, experimental_bar: u64) -> $return_type { + $self.refund.experimental_bar = Some(experimental_bar); + $return_value + } } } impl<'a> RefundBuilder<'a, secp256k1::SignOnly> { @@ -437,6 +457,10 @@ pub(super) struct RefundContents { payer_signing_pubkey: PublicKey, payer_note: Option, paths: Option>, + #[cfg(test)] + experimental_foo: Option, + #[cfg(test)] + experimental_bar: Option, } impl Refund { @@ -770,7 +794,17 @@ impl RefundContents { paths: self.paths.as_ref(), }; - (payer, offer, invoice_request) + let experimental_offer = ExperimentalOfferTlvStreamRef { + #[cfg(test)] + experimental_foo: self.experimental_foo, + }; + + let experimental_invoice_request = ExperimentalInvoiceRequestTlvStreamRef { + #[cfg(test)] + experimental_bar: self.experimental_bar, + }; + + (payer, offer, invoice_request, experimental_offer, experimental_invoice_request) } } @@ -793,12 +827,17 @@ impl Writeable for RefundContents { } } -type RefundTlvStream = (PayerTlvStream, OfferTlvStream, InvoiceRequestTlvStream); +type RefundTlvStream = ( + PayerTlvStream, OfferTlvStream, InvoiceRequestTlvStream, ExperimentalOfferTlvStream, + ExperimentalInvoiceRequestTlvStream, +); type RefundTlvStreamRef<'a> = ( PayerTlvStreamRef<'a>, OfferTlvStreamRef<'a>, InvoiceRequestTlvStreamRef<'a>, + ExperimentalOfferTlvStreamRef, + ExperimentalInvoiceRequestTlvStreamRef, ); impl CursorReadable for RefundTlvStream { @@ -806,8 +845,10 @@ impl CursorReadable for RefundTlvStream { let payer = CursorReadable::read(r)?; let offer = CursorReadable::read(r)?; let invoice_request = CursorReadable::read(r)?; + let experimental_offer = CursorReadable::read(r)?; + let experimental_invoice_request = CursorReadable::read(r)?; - Ok((payer, offer, invoice_request)) + Ok((payer, offer, invoice_request, experimental_offer, experimental_invoice_request)) } } @@ -849,6 +890,14 @@ impl TryFrom for RefundContents { InvoiceRequestTlvStream { chain, amount, features, quantity, payer_id, payer_note, paths }, + ExperimentalOfferTlvStream { + #[cfg(test)] + experimental_foo, + }, + ExperimentalInvoiceRequestTlvStream { + #[cfg(test)] + experimental_bar, + }, ) = tlv_stream; let payer = match payer_metadata { @@ -909,6 +958,10 @@ impl TryFrom for RefundContents { Ok(RefundContents { payer, description, absolute_expiry, issuer, chain, amount_msats, features, quantity, payer_signing_pubkey, payer_note, paths, + #[cfg(test)] + experimental_foo, + #[cfg(test)] + experimental_bar, }) } } @@ -944,9 +997,9 @@ mod tests { use crate::types::features::{InvoiceRequestFeatures, OfferFeatures}; use crate::ln::inbound_payment::ExpandedKey; use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT}; - use crate::offers::invoice_request::InvoiceRequestTlvStreamRef; + use crate::offers::invoice_request::{EXPERIMENTAL_INVOICE_REQUEST_TYPES, ExperimentalInvoiceRequestTlvStreamRef, INVOICE_REQUEST_TYPES, InvoiceRequestTlvStreamRef}; use crate::offers::nonce::Nonce; - use crate::offers::offer::OfferTlvStreamRef; + use crate::offers::offer::{ExperimentalOfferTlvStreamRef, OfferTlvStreamRef}; use crate::offers::parse::{Bolt12ParseError, Bolt12SemanticError}; use crate::offers::payer::PayerTlvStreamRef; use crate::offers::test_utils::*; @@ -1014,6 +1067,12 @@ mod tests { payer_note: None, paths: None, }, + ExperimentalOfferTlvStreamRef { + experimental_foo: None, + }, + ExperimentalInvoiceRequestTlvStreamRef { + experimental_bar: None, + }, ), ); @@ -1042,6 +1101,8 @@ mod tests { let refund = RefundBuilder ::deriving_signing_pubkey(node_id, &expanded_key, nonce, &secp_ctx, 1000, payment_id) .unwrap() + .experimental_foo(42) + .experimental_bar(42) .build().unwrap(); assert_eq!(refund.payer_signing_pubkey(), node_id); @@ -1049,6 +1110,7 @@ mod tests { let invoice = refund .respond_with_no_std(payment_paths(), payment_hash(), recipient_pubkey(), now()) .unwrap() + .experimental_baz(42) .build().unwrap() .sign(recipient_sign).unwrap(); match invoice.verify_using_metadata(&expanded_key, &secp_ctx) { @@ -1109,12 +1171,15 @@ mod tests { ::deriving_signing_pubkey(node_id, &expanded_key, nonce, &secp_ctx, 1000, payment_id) .unwrap() .path(blinded_path) + .experimental_foo(42) + .experimental_bar(42) .build().unwrap(); assert_ne!(refund.payer_signing_pubkey(), node_id); let invoice = refund .respond_with_no_std(payment_paths(), payment_hash(), recipient_pubkey(), now()) .unwrap() + .experimental_baz(42) .build().unwrap() .sign(recipient_sign).unwrap(); assert!(invoice.verify_using_metadata(&expanded_key, &secp_ctx).is_err()); @@ -1166,7 +1231,7 @@ mod tests { .absolute_expiry(future_expiry) .build() .unwrap(); - let (_, tlv_stream, _) = refund.as_tlv_stream(); + let (_, tlv_stream, _, _, _) = refund.as_tlv_stream(); #[cfg(feature = "std")] assert!(!refund.is_expired()); assert!(!refund.is_expired_no_std(now)); @@ -1178,7 +1243,7 @@ mod tests { .absolute_expiry(past_expiry) .build() .unwrap(); - let (_, tlv_stream, _) = refund.as_tlv_stream(); + let (_, tlv_stream, _, _, _) = refund.as_tlv_stream(); #[cfg(feature = "std")] assert!(refund.is_expired()); assert!(refund.is_expired_no_std(now)); @@ -1210,7 +1275,7 @@ mod tests { .path(paths[1].clone()) .build() .unwrap(); - let (_, _, invoice_request_tlv_stream) = refund.as_tlv_stream(); + let (_, _, invoice_request_tlv_stream, _, _) = refund.as_tlv_stream(); assert_eq!(refund.payer_signing_pubkey(), pubkey(42)); assert_eq!(refund.paths(), paths.as_slice()); assert_ne!(pubkey(42), pubkey(44)); @@ -1224,7 +1289,7 @@ mod tests { .issuer("bar".into()) .build() .unwrap(); - let (_, tlv_stream, _) = refund.as_tlv_stream(); + let (_, tlv_stream, _, _, _) = refund.as_tlv_stream(); assert_eq!(refund.issuer(), Some(PrintableString("bar"))); assert_eq!(tlv_stream.issuer, Some(&String::from("bar"))); @@ -1233,7 +1298,7 @@ mod tests { .issuer("baz".into()) .build() .unwrap(); - let (_, tlv_stream, _) = refund.as_tlv_stream(); + let (_, tlv_stream, _, _, _) = refund.as_tlv_stream(); assert_eq!(refund.issuer(), Some(PrintableString("baz"))); assert_eq!(tlv_stream.issuer, Some(&String::from("baz"))); } @@ -1246,14 +1311,14 @@ mod tests { let refund = RefundBuilder::new(vec![1; 32], payer_pubkey(), 1000).unwrap() .chain(Network::Bitcoin) .build().unwrap(); - let (_, _, tlv_stream) = refund.as_tlv_stream(); + let (_, _, tlv_stream, _, _) = refund.as_tlv_stream(); assert_eq!(refund.chain(), mainnet); assert_eq!(tlv_stream.chain, None); let refund = RefundBuilder::new(vec![1; 32], payer_pubkey(), 1000).unwrap() .chain(Network::Testnet) .build().unwrap(); - let (_, _, tlv_stream) = refund.as_tlv_stream(); + let (_, _, tlv_stream, _, _) = refund.as_tlv_stream(); assert_eq!(refund.chain(), testnet); assert_eq!(tlv_stream.chain, Some(&testnet)); @@ -1261,7 +1326,7 @@ mod tests { .chain(Network::Regtest) .chain(Network::Testnet) .build().unwrap(); - let (_, _, tlv_stream) = refund.as_tlv_stream(); + let (_, _, tlv_stream, _, _) = refund.as_tlv_stream(); assert_eq!(refund.chain(), testnet); assert_eq!(tlv_stream.chain, Some(&testnet)); } @@ -1271,7 +1336,7 @@ mod tests { let refund = RefundBuilder::new(vec![1; 32], payer_pubkey(), 1000).unwrap() .quantity(10) .build().unwrap(); - let (_, _, tlv_stream) = refund.as_tlv_stream(); + let (_, _, tlv_stream, _, _) = refund.as_tlv_stream(); assert_eq!(refund.quantity(), Some(10)); assert_eq!(tlv_stream.quantity, Some(10)); @@ -1279,7 +1344,7 @@ mod tests { .quantity(10) .quantity(1) .build().unwrap(); - let (_, _, tlv_stream) = refund.as_tlv_stream(); + let (_, _, tlv_stream, _, _) = refund.as_tlv_stream(); assert_eq!(refund.quantity(), Some(1)); assert_eq!(tlv_stream.quantity, Some(1)); } @@ -1289,7 +1354,7 @@ mod tests { let refund = RefundBuilder::new(vec![1; 32], payer_pubkey(), 1000).unwrap() .payer_note("bar".into()) .build().unwrap(); - let (_, _, tlv_stream) = refund.as_tlv_stream(); + let (_, _, tlv_stream, _, _) = refund.as_tlv_stream(); assert_eq!(refund.payer_note(), Some(PrintableString("bar"))); assert_eq!(tlv_stream.payer_note, Some(&String::from("bar"))); @@ -1297,7 +1362,7 @@ mod tests { .payer_note("bar".into()) .payer_note("baz".into()) .build().unwrap(); - let (_, _, tlv_stream) = refund.as_tlv_stream(); + let (_, _, tlv_stream, _, _) = refund.as_tlv_stream(); assert_eq!(refund.payer_note(), Some(PrintableString("baz"))); assert_eq!(tlv_stream.payer_note, Some(&String::from("baz"))); } @@ -1522,7 +1587,81 @@ mod tests { } #[test] - fn fails_parsing_refund_with_extra_tlv_records() { + fn parses_refund_with_unknown_tlv_records() { + const UNKNOWN_ODD_TYPE: u64 = INVOICE_REQUEST_TYPES.end - 1; + assert!(UNKNOWN_ODD_TYPE % 2 == 1); + + let refund = RefundBuilder::new(vec![1; 32], payer_pubkey(), 1000).unwrap() + .build().unwrap(); + + let mut encoded_refund = Vec::new(); + refund.write(&mut encoded_refund).unwrap(); + BigSize(UNKNOWN_ODD_TYPE).write(&mut encoded_refund).unwrap(); + BigSize(32).write(&mut encoded_refund).unwrap(); + [42u8; 32].write(&mut encoded_refund).unwrap(); + + match Refund::try_from(encoded_refund.clone()) { + Ok(refund) => assert_eq!(refund.bytes, encoded_refund), + Err(e) => panic!("error parsing refund: {:?}", e), + } + + const UNKNOWN_EVEN_TYPE: u64 = INVOICE_REQUEST_TYPES.end - 2; + assert!(UNKNOWN_EVEN_TYPE % 2 == 0); + + let refund = RefundBuilder::new(vec![1; 32], payer_pubkey(), 1000).unwrap() + .build().unwrap(); + + let mut encoded_refund = Vec::new(); + refund.write(&mut encoded_refund).unwrap(); + BigSize(UNKNOWN_EVEN_TYPE).write(&mut encoded_refund).unwrap(); + BigSize(32).write(&mut encoded_refund).unwrap(); + [42u8; 32].write(&mut encoded_refund).unwrap(); + + match Refund::try_from(encoded_refund) { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, Bolt12ParseError::Decode(DecodeError::UnknownRequiredFeature)), + } + } + + #[test] + fn parses_refund_with_experimental_tlv_records() { + const UNKNOWN_ODD_TYPE: u64 = EXPERIMENTAL_INVOICE_REQUEST_TYPES.start + 1; + assert!(UNKNOWN_ODD_TYPE % 2 == 1); + + let refund = RefundBuilder::new(vec![1; 32], payer_pubkey(), 1000).unwrap() + .build().unwrap(); + + let mut encoded_refund = Vec::new(); + refund.write(&mut encoded_refund).unwrap(); + BigSize(UNKNOWN_ODD_TYPE).write(&mut encoded_refund).unwrap(); + BigSize(32).write(&mut encoded_refund).unwrap(); + [42u8; 32].write(&mut encoded_refund).unwrap(); + + match Refund::try_from(encoded_refund.clone()) { + Ok(refund) => assert_eq!(refund.bytes, encoded_refund), + Err(e) => panic!("error parsing refund: {:?}", e), + } + + const UNKNOWN_EVEN_TYPE: u64 = EXPERIMENTAL_INVOICE_REQUEST_TYPES.start; + assert!(UNKNOWN_EVEN_TYPE % 2 == 0); + + let refund = RefundBuilder::new(vec![1; 32], payer_pubkey(), 1000).unwrap() + .build().unwrap(); + + let mut encoded_refund = Vec::new(); + refund.write(&mut encoded_refund).unwrap(); + BigSize(UNKNOWN_EVEN_TYPE).write(&mut encoded_refund).unwrap(); + BigSize(32).write(&mut encoded_refund).unwrap(); + [42u8; 32].write(&mut encoded_refund).unwrap(); + + match Refund::try_from(encoded_refund) { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, Bolt12ParseError::Decode(DecodeError::UnknownRequiredFeature)), + } + } + + #[test] + fn fails_parsing_refund_with_out_of_range_tlv_records() { let secp_ctx = Secp256k1::new(); let keys = Keypair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap()); let refund = RefundBuilder::new(vec![1; 32], keys.public_key(), 1000).unwrap() @@ -1538,5 +1677,16 @@ mod tests { Ok(_) => panic!("expected error"), Err(e) => assert_eq!(e, Bolt12ParseError::Decode(DecodeError::InvalidValue)), } + + let mut encoded_refund = Vec::new(); + refund.write(&mut encoded_refund).unwrap(); + BigSize(EXPERIMENTAL_INVOICE_REQUEST_TYPES.end).write(&mut encoded_refund).unwrap(); + BigSize(32).write(&mut encoded_refund).unwrap(); + [42u8; 32].write(&mut encoded_refund).unwrap(); + + match Refund::try_from(encoded_refund) { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, Bolt12ParseError::Decode(DecodeError::InvalidValue)), + } } } diff --git a/lightning/src/offers/static_invoice.rs b/lightning/src/offers/static_invoice.rs index 107e9c69892..50655c392c8 100644 --- a/lightning/src/offers/static_invoice.rs +++ b/lightning/src/offers/static_invoice.rs @@ -15,17 +15,22 @@ use crate::io; use crate::ln::inbound_payment::ExpandedKey; use crate::ln::msgs::DecodeError; use crate::offers::invoice::{ - check_invoice_signing_pubkey, construct_payment_paths, filter_fallbacks, FallbackAddress, + check_invoice_signing_pubkey, construct_payment_paths, filter_fallbacks, + ExperimentalInvoiceTlvStream, ExperimentalInvoiceTlvStreamRef, FallbackAddress, InvoiceTlvStream, InvoiceTlvStreamRef, }; +#[cfg(test)] +use crate::offers::invoice_macros::invoice_builder_methods_test; use crate::offers::invoice_macros::{invoice_accessors_common, invoice_builder_methods_common}; 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, ExperimentalOfferTlvStream, ExperimentalOfferTlvStreamRef, Offer, OfferContents, + OfferTlvStream, OfferTlvStreamRef, Quantity, EXPERIMENTAL_OFFER_TYPES, OFFER_TYPES, }; use crate::offers::parse::{Bolt12ParseError, Bolt12SemanticError, ParsedMessage}; use crate::types::features::{Bolt12InvoiceFeatures, OfferFeatures}; @@ -79,6 +84,8 @@ struct InvoiceContents { features: Bolt12InvoiceFeatures, signing_pubkey: PublicKey, message_paths: Vec, + #[cfg(test)] + experimental_baz: Option, } /// Builds a [`StaticInvoice`] from an [`Offer`]. @@ -130,10 +137,9 @@ impl<'a> StaticInvoiceBuilder<'a> { Ok(Self { offer_bytes: &offer.bytes, invoice, keys }) } - /// Builds a signed [`StaticInvoice`] after checking for valid semantics. - pub fn build_and_sign( - self, secp_ctx: &Secp256k1, - ) -> Result { + /// Builds an [`UnsignedStaticInvoice`] after checking for valid semantics, returning it along with + /// the [`Keypair`] needed to sign it. + pub fn build(self) -> Result<(UnsignedStaticInvoice, Keypair), Bolt12SemanticError> { #[cfg(feature = "std")] { if self.invoice.is_offer_expired() { @@ -149,7 +155,14 @@ impl<'a> StaticInvoiceBuilder<'a> { } let Self { offer_bytes, invoice, keys } = self; - let unsigned_invoice = UnsignedStaticInvoice::new(&offer_bytes, invoice); + Ok((UnsignedStaticInvoice::new(&offer_bytes, invoice), keys)) + } + + /// Builds a signed [`StaticInvoice`] after checking for valid semantics. + pub fn build_and_sign( + self, secp_ctx: &Secp256k1, + ) -> Result { + let (unsigned_invoice, keys) = self.build()?; let invoice = unsigned_invoice .sign(|message: &UnsignedStaticInvoice| { Ok(secp_ctx.sign_schnorr_no_aux_rand(message.tagged_hash.as_digest(), &keys)) @@ -159,11 +172,15 @@ impl<'a> StaticInvoiceBuilder<'a> { } invoice_builder_methods_common!(self, Self, self.invoice, Self, self, StaticInvoice, mut); + + #[cfg(test)] + invoice_builder_methods_test!(self, Self, self.invoice, Self, self, mut); } /// A semantically valid [`StaticInvoice`] that hasn't been signed. pub struct UnsignedStaticInvoice { bytes: Vec, + experimental_bytes: Vec, contents: InvoiceContents, tagged_hash: TaggedHash, } @@ -269,16 +286,50 @@ macro_rules! invoice_accessors_signing_pubkey { impl UnsignedStaticInvoice { fn new(offer_bytes: &Vec, 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 (_, invoice_tlv_stream, _, experimental_invoice_tlv_stream) = contents.as_tlv_stream(); + + // 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 + + experimental_invoice_tlv_stream.serialized_length(), + ); - let mut bytes = Vec::new(); - unsigned_tlv_stream.write(&mut bytes).unwrap(); + // 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 tagged_hash = TaggedHash::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &bytes); + 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) + + experimental_invoice_tlv_stream.serialized_length(), + ); + + for record in experimental_tlv_stream { + record.write(&mut experimental_bytes).unwrap(); + } + + experimental_invoice_tlv_stream.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. @@ -292,6 +343,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 }) } @@ -341,12 +401,10 @@ impl StaticInvoice { } pub(crate) fn from_same_offer(&self, invreq: &InvoiceRequest) -> bool { - let invoice_offer_tlv_stream = TlvStream::new(&self.bytes) - .range(OFFER_TYPES) - .map(|tlv_record| tlv_record.record_bytes); - let invreq_offer_tlv_stream = TlvStream::new(invreq.bytes()) - .range(OFFER_TYPES) - .map(|tlv_record| tlv_record.record_bytes); + let invoice_offer_tlv_stream = + Offer::tlv_stream_iter(&self.bytes).map(|tlv_record| tlv_record.record_bytes); + let invreq_offer_tlv_stream = + Offer::tlv_stream_iter(invreq.bytes()).map(|tlv_record| tlv_record.record_bytes); invoice_offer_tlv_stream.eq(invreq_offer_tlv_stream) } } @@ -375,6 +433,8 @@ impl InvoiceContents { fallbacks: None, features: Bolt12InvoiceFeatures::empty(), signing_pubkey, + #[cfg(test)] + experimental_baz: None, } } @@ -400,7 +460,14 @@ impl InvoiceContents { payment_hash: None, }; - (self.offer.as_tlv_stream(), invoice) + let experimental_invoice = ExperimentalInvoiceTlvStreamRef { + #[cfg(test)] + experimental_baz: self.experimental_baz, + }; + + let (offer, experimental_offer) = self.offer.as_tlv_stream(); + + (offer, invoice, experimental_offer, experimental_invoice) } fn chain(&self) -> ChainHash { @@ -497,29 +564,54 @@ impl TryFrom> for StaticInvoice { } } -type FullInvoiceTlvStream = (OfferTlvStream, InvoiceTlvStream, SignatureTlvStream); +type FullInvoiceTlvStream = ( + OfferTlvStream, + InvoiceTlvStream, + SignatureTlvStream, + ExperimentalOfferTlvStream, + ExperimentalInvoiceTlvStream, +); impl CursorReadable for FullInvoiceTlvStream { fn read>(r: &mut io::Cursor) -> Result { let offer = CursorReadable::read(r)?; let invoice = CursorReadable::read(r)?; let signature = CursorReadable::read(r)?; + let experimental_offer = CursorReadable::read(r)?; + let experimental_invoice = CursorReadable::read(r)?; - Ok((offer, invoice, signature)) + Ok((offer, invoice, signature, experimental_offer, experimental_invoice)) } } -type PartialInvoiceTlvStream = (OfferTlvStream, InvoiceTlvStream); +type PartialInvoiceTlvStream = + (OfferTlvStream, InvoiceTlvStream, ExperimentalOfferTlvStream, ExperimentalInvoiceTlvStream); -type PartialInvoiceTlvStreamRef<'a> = (OfferTlvStreamRef<'a>, InvoiceTlvStreamRef<'a>); +type PartialInvoiceTlvStreamRef<'a> = ( + OfferTlvStreamRef<'a>, + InvoiceTlvStreamRef<'a>, + ExperimentalOfferTlvStreamRef, + ExperimentalInvoiceTlvStreamRef, +); impl TryFrom> for StaticInvoice { type Error = Bolt12ParseError; fn try_from(invoice: ParsedMessage) -> Result { let ParsedMessage { bytes, tlv_stream } = invoice; - let (offer_tlv_stream, invoice_tlv_stream, SignatureTlvStream { signature }) = tlv_stream; - let contents = InvoiceContents::try_from((offer_tlv_stream, invoice_tlv_stream))?; + let ( + offer_tlv_stream, + invoice_tlv_stream, + SignatureTlvStream { signature }, + experimental_offer_tlv_stream, + experimental_invoice_tlv_stream, + ) = tlv_stream; + let contents = InvoiceContents::try_from(( + offer_tlv_stream, + invoice_tlv_stream, + experimental_offer_tlv_stream, + experimental_invoice_tlv_stream, + ))?; let signature = match signature { None => { @@ -555,6 +647,11 @@ impl TryFrom for InvoiceContents { payment_hash, amount, }, + experimental_offer_tlv_stream, + ExperimentalInvoiceTlvStream { + #[cfg(test)] + experimental_baz, + }, ) = tlv_stream; if payment_hash.is_some() { @@ -587,7 +684,7 @@ impl TryFrom for InvoiceContents { } Ok(InvoiceContents { - offer: OfferContents::try_from(offer_tlv_stream)?, + offer: OfferContents::try_from((offer_tlv_stream, experimental_offer_tlv_stream))?, payment_paths, message_paths, created_at, @@ -595,6 +692,8 @@ impl TryFrom for InvoiceContents { fallbacks, features, signing_pubkey, + #[cfg(test)] + experimental_baz, }) } } @@ -605,14 +704,20 @@ mod tests { use crate::blinded_path::BlindedHop; use crate::ln::inbound_payment::ExpandedKey; use crate::ln::msgs::DecodeError; - use crate::offers::invoice::InvoiceTlvStreamRef; + use crate::offers::invoice::{ + ExperimentalInvoiceTlvStreamRef, InvoiceTlvStreamRef, EXPERIMENTAL_INVOICE_TYPES, + INVOICE_TYPES, + }; use crate::offers::merkle; - use crate::offers::merkle::{SignatureTlvStreamRef, TaggedHash}; + use crate::offers::merkle::{SignatureTlvStreamRef, TaggedHash, TlvStream}; use crate::offers::nonce::Nonce; - use crate::offers::offer::{Offer, OfferBuilder, OfferTlvStreamRef, Quantity}; + use crate::offers::offer::{ + ExperimentalOfferTlvStreamRef, Offer, OfferBuilder, OfferTlvStreamRef, Quantity, + }; use crate::offers::parse::{Bolt12ParseError, Bolt12SemanticError}; use crate::offers::static_invoice::{ - StaticInvoice, StaticInvoiceBuilder, DEFAULT_RELATIVE_EXPIRY, SIGNATURE_TAG, + StaticInvoice, StaticInvoiceBuilder, UnsignedStaticInvoice, DEFAULT_RELATIVE_EXPIRY, + SIGNATURE_TAG, }; use crate::offers::test_utils::*; use crate::sign::KeyMaterial; @@ -623,27 +728,47 @@ mod tests { use bitcoin::Network; use core::time::Duration; - type FullInvoiceTlvStreamRef<'a> = - (OfferTlvStreamRef<'a>, InvoiceTlvStreamRef<'a>, SignatureTlvStreamRef<'a>); + type FullInvoiceTlvStreamRef<'a> = ( + OfferTlvStreamRef<'a>, + InvoiceTlvStreamRef<'a>, + SignatureTlvStreamRef<'a>, + ExperimentalOfferTlvStreamRef, + ExperimentalInvoiceTlvStreamRef, + ); impl StaticInvoice { fn as_tlv_stream(&self) -> FullInvoiceTlvStreamRef { - let (offer_tlv_stream, invoice_tlv_stream) = self.contents.as_tlv_stream(); + let ( + offer_tlv_stream, + invoice_tlv_stream, + experimental_offer_tlv_stream, + experimental_invoice_tlv_stream, + ) = self.contents.as_tlv_stream(); ( offer_tlv_stream, invoice_tlv_stream, SignatureTlvStreamRef { signature: Some(&self.signature) }, + experimental_offer_tlv_stream, + experimental_invoice_tlv_stream, ) } } fn tlv_stream_to_bytes( - tlv_stream: &(OfferTlvStreamRef, InvoiceTlvStreamRef, SignatureTlvStreamRef), + tlv_stream: &( + OfferTlvStreamRef, + InvoiceTlvStreamRef, + SignatureTlvStreamRef, + ExperimentalOfferTlvStreamRef, + ExperimentalInvoiceTlvStreamRef, + ), ) -> Vec { let mut buffer = Vec::new(); tlv_stream.0.write(&mut buffer).unwrap(); tlv_stream.1.write(&mut buffer).unwrap(); tlv_stream.2.write(&mut buffer).unwrap(); + tlv_stream.3.write(&mut buffer).unwrap(); + tlv_stream.4.write(&mut buffer).unwrap(); buffer } @@ -773,6 +898,8 @@ mod tests { message_paths: Some(&paths), }, SignatureTlvStreamRef { signature: Some(&invoice.signature()) }, + ExperimentalOfferTlvStreamRef { experimental_foo: None }, + ExperimentalInvoiceTlvStreamRef { experimental_baz: None }, ) ); @@ -840,6 +967,52 @@ mod tests { } } + #[test] + fn builds_invoice_from_offer_using_derived_key() { + let node_id = recipient_pubkey(); + let now = now(); + let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32])); + let entropy = FixedEntropy {}; + let nonce = Nonce::from_entropy_source(&entropy); + let secp_ctx = Secp256k1::new(); + + let offer = OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, nonce, &secp_ctx) + .path(blinded_path()) + .experimental_foo(42) + .build() + .unwrap(); + + if let Err(e) = StaticInvoiceBuilder::for_offer_using_derived_keys( + &offer, + payment_paths(), + vec![blinded_path()], + now, + &expanded_key, + nonce, + &secp_ctx, + ) + .unwrap() + .build_and_sign(&secp_ctx) + { + panic!("error building invoice: {:?}", e); + } + + let expanded_key = ExpandedKey::new(&KeyMaterial([41; 32])); + if let Err(e) = StaticInvoiceBuilder::for_offer_using_derived_keys( + &offer, + payment_paths(), + vec![blinded_path()], + now, + &expanded_key, + nonce, + &secp_ctx, + ) { + assert_eq!(e, Bolt12SemanticError::InvalidMetadata); + } else { + panic!("expected error") + } + } + #[test] fn fails_build_with_missing_paths() { let node_id = recipient_pubkey(); @@ -887,7 +1060,7 @@ mod tests { // Error if offer paths are missing. let mut offer_without_paths = valid_offer.clone(); - let mut offer_tlv_stream = offer_without_paths.as_tlv_stream(); + let (mut offer_tlv_stream, _) = offer_without_paths.as_tlv_stream(); offer_tlv_stream.paths.take(); let mut buffer = Vec::new(); offer_tlv_stream.write(&mut buffer).unwrap(); @@ -923,7 +1096,7 @@ mod tests { .unwrap(); let mut offer_missing_issuer_id = valid_offer.clone(); - let mut offer_tlv_stream = offer_missing_issuer_id.as_tlv_stream(); + let (mut offer_tlv_stream, _) = offer_missing_issuer_id.as_tlv_stream(); offer_tlv_stream.issuer_id.take(); let mut buffer = Vec::new(); offer_tlv_stream.write(&mut buffer).unwrap(); @@ -1185,7 +1358,263 @@ mod tests { } #[test] - fn fails_parsing_invoice_with_extra_tlv_records() { + fn parses_invoice_with_unknown_tlv_records() { + let node_id = recipient_pubkey(); + let payment_paths = payment_paths(); + let now = now(); + let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32])); + let entropy = FixedEntropy {}; + let nonce = Nonce::from_entropy_source(&entropy); + let secp_ctx = Secp256k1::new(); + + let offer = OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, nonce, &secp_ctx) + .path(blinded_path()) + .build() + .unwrap(); + + const UNKNOWN_ODD_TYPE: u64 = INVOICE_TYPES.end - 1; + assert!(UNKNOWN_ODD_TYPE % 2 == 1); + + let (mut unsigned_invoice, keys) = StaticInvoiceBuilder::for_offer_using_derived_keys( + &offer, + payment_paths.clone(), + vec![blinded_path()], + now, + &expanded_key, + nonce, + &secp_ctx, + ) + .unwrap() + .build() + .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); + + let invoice = unsigned_invoice + .sign(|message: &UnsignedStaticInvoice| { + Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys)) + }) + .unwrap(); + + let mut encoded_invoice = Vec::new(); + invoice.write(&mut encoded_invoice).unwrap(); + + match StaticInvoice::try_from(encoded_invoice.clone()) { + Ok(invoice) => assert_eq!(invoice.bytes, encoded_invoice), + Err(e) => panic!("error parsing invoice: {:?}", e), + } + + const UNKNOWN_EVEN_TYPE: u64 = INVOICE_TYPES.end - 2; + assert!(UNKNOWN_EVEN_TYPE % 2 == 0); + + let (mut unsigned_invoice, keys) = StaticInvoiceBuilder::for_offer_using_derived_keys( + &offer, + payment_paths.clone(), + vec![blinded_path()], + now, + &expanded_key, + nonce, + &secp_ctx, + ) + .unwrap() + .build() + .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); + + let invoice = unsigned_invoice + .sign(|message: &UnsignedStaticInvoice| { + Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys)) + }) + .unwrap(); + + let mut encoded_invoice = Vec::new(); + invoice.write(&mut encoded_invoice).unwrap(); + + match StaticInvoice::try_from(encoded_invoice) { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, Bolt12ParseError::Decode(DecodeError::UnknownRequiredFeature)), + } + } + + #[test] + fn parses_invoice_with_experimental_tlv_records() { + let node_id = recipient_pubkey(); + let payment_paths = payment_paths(); + let now = now(); + let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32])); + let entropy = FixedEntropy {}; + let nonce = Nonce::from_entropy_source(&entropy); + let secp_ctx = Secp256k1::new(); + + let offer = OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, nonce, &secp_ctx) + .path(blinded_path()) + .build() + .unwrap(); + + let invoice = StaticInvoiceBuilder::for_offer_using_derived_keys( + &offer, + payment_paths.clone(), + vec![blinded_path()], + now, + &expanded_key, + nonce, + &secp_ctx, + ) + .unwrap() + .experimental_baz(42) + .build_and_sign(&secp_ctx) + .unwrap(); + + let mut encoded_invoice = Vec::new(); + invoice.write(&mut encoded_invoice).unwrap(); + + assert!(StaticInvoice::try_from(encoded_invoice).is_ok()); + + const UNKNOWN_ODD_TYPE: u64 = EXPERIMENTAL_INVOICE_TYPES.start + 1; + assert!(UNKNOWN_ODD_TYPE % 2 == 1); + + let (mut unsigned_invoice, keys) = StaticInvoiceBuilder::for_offer_using_derived_keys( + &offer, + payment_paths.clone(), + vec![blinded_path()], + now, + &expanded_key, + nonce, + &secp_ctx, + ) + .unwrap() + .build() + .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.experimental_bytes.extend_from_slice(&unknown_bytes); + + let tlv_stream = TlvStream::new(&unsigned_invoice.bytes) + .chain(TlvStream::new(&unsigned_invoice.experimental_bytes)); + unsigned_invoice.tagged_hash = TaggedHash::from_tlv_stream(SIGNATURE_TAG, tlv_stream); + + let invoice = unsigned_invoice + .sign(|message: &UnsignedStaticInvoice| { + Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys)) + }) + .unwrap(); + + let mut encoded_invoice = Vec::new(); + invoice.write(&mut encoded_invoice).unwrap(); + + match StaticInvoice::try_from(encoded_invoice.clone()) { + Ok(invoice) => assert_eq!(invoice.bytes, encoded_invoice), + Err(e) => panic!("error parsing invoice: {:?}", e), + } + + const UNKNOWN_EVEN_TYPE: u64 = EXPERIMENTAL_INVOICE_TYPES.start; + assert!(UNKNOWN_EVEN_TYPE % 2 == 0); + + let (mut unsigned_invoice, keys) = StaticInvoiceBuilder::for_offer_using_derived_keys( + &offer, + payment_paths.clone(), + vec![blinded_path()], + now, + &expanded_key, + nonce, + &secp_ctx, + ) + .unwrap() + .build() + .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.experimental_bytes.extend_from_slice(&unknown_bytes); + + let tlv_stream = TlvStream::new(&unsigned_invoice.bytes) + .chain(TlvStream::new(&unsigned_invoice.experimental_bytes)); + unsigned_invoice.tagged_hash = TaggedHash::from_tlv_stream(SIGNATURE_TAG, tlv_stream); + + let invoice = unsigned_invoice + .sign(|message: &UnsignedStaticInvoice| { + Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys)) + }) + .unwrap(); + + let mut encoded_invoice = Vec::new(); + invoice.write(&mut encoded_invoice).unwrap(); + + match StaticInvoice::try_from(encoded_invoice) { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, Bolt12ParseError::Decode(DecodeError::UnknownRequiredFeature)), + } + + let offer = OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, nonce, &secp_ctx) + .path(blinded_path()) + .build() + .unwrap(); + + let invoice = StaticInvoiceBuilder::for_offer_using_derived_keys( + &offer, + payment_paths.clone(), + vec![blinded_path()], + now, + &expanded_key, + nonce, + &secp_ctx, + ) + .unwrap() + .build_and_sign(&secp_ctx) + .unwrap(); + + let mut encoded_invoice = Vec::new(); + invoice.write(&mut encoded_invoice).unwrap(); + + BigSize(UNKNOWN_ODD_TYPE).write(&mut encoded_invoice).unwrap(); + BigSize(32).write(&mut encoded_invoice).unwrap(); + [42u8; 32].write(&mut encoded_invoice).unwrap(); + + match StaticInvoice::try_from(encoded_invoice) { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!( + e, + Bolt12ParseError::InvalidSignature(secp256k1::Error::IncorrectSignature) + ), + } + } + + #[test] + fn fails_parsing_invoice_with_out_of_range_tlv_records() { let invoice = invoice(); let mut encoded_invoice = Vec::new(); invoice.write(&mut encoded_invoice).unwrap(); diff --git a/lightning/src/util/ser.rs b/lightning/src/util/ser.rs index c37c326790b..e4158e31bf5 100644 --- a/lightning/src/util/ser.rs +++ b/lightning/src/util/ser.rs @@ -1402,53 +1402,38 @@ impl Writeable for RwLock { } } -impl Readable for (A, B) { - fn read(r: &mut R) -> Result { - let a: A = Readable::read(r)?; - let b: B = Readable::read(r)?; - Ok((a, b)) - } -} -impl Writeable for (A, B) { - fn write(&self, w: &mut W) -> Result<(), io::Error> { - self.0.write(w)?; - self.1.write(w) - } -} +macro_rules! impl_tuple_ser { + ($($i: ident : $type: tt),*) => { + impl<$($type),*> Readable for ($($type),*) + where $( + $type: Readable, + )* + { + fn read(r: &mut R) -> Result { + Ok(($(<$type as Readable>::read(r)?),*)) + } + } -impl Readable for (A, B, C) { - fn read(r: &mut R) -> Result { - let a: A = Readable::read(r)?; - let b: B = Readable::read(r)?; - let c: C = Readable::read(r)?; - Ok((a, b, c)) - } -} -impl Writeable for (A, B, C) { - fn write(&self, w: &mut W) -> Result<(), io::Error> { - self.0.write(w)?; - self.1.write(w)?; - self.2.write(w) + impl<$($type),*> Writeable for ($($type),*) + where $( + $type: Writeable, + )* + { + fn write(&self, w: &mut W) -> Result<(), io::Error> { + let ($($i),*) = self; + $($i.write(w)?;)* + Ok(()) + } + } } } -impl Readable for (A, B, C, D) { - fn read(r: &mut R) -> Result { - let a: A = Readable::read(r)?; - let b: B = Readable::read(r)?; - let c: C = Readable::read(r)?; - let d: D = Readable::read(r)?; - Ok((a, b, c, d)) - } -} -impl Writeable for (A, B, C, D) { - fn write(&self, w: &mut W) -> Result<(), io::Error> { - self.0.write(w)?; - self.1.write(w)?; - self.2.write(w)?; - self.3.write(w) - } -} +impl_tuple_ser!(a: A, b: B); +impl_tuple_ser!(a: A, b: B, c: C); +impl_tuple_ser!(a: A, b: B, c: C, d: D); +impl_tuple_ser!(a: A, b: B, c: C, d: D, e: E); +impl_tuple_ser!(a: A, b: B, c: C, d: D, e: E, f: F); +impl_tuple_ser!(a: A, b: B, c: C, d: D, e: E, f: F, g: G); impl Writeable for () { fn write(&self, _: &mut W) -> Result<(), io::Error> { diff --git a/lightning/src/util/ser_macros.rs b/lightning/src/util/ser_macros.rs index d4428697b4d..0703aac9e84 100644 --- a/lightning/src/util/ser_macros.rs +++ b/lightning/src/util/ser_macros.rs @@ -952,7 +952,7 @@ macro_rules! impl_writeable_tlv_based { /// [`Readable`]: crate::util::ser::Readable /// [`Writeable`]: crate::util::ser::Writeable macro_rules! tlv_stream { - ($name:ident, $nameref:ident, $range:expr, { + ($name:ident, $nameref:ident $(<$lifetime:lifetime>)?, $range:expr, { $(($type:expr, $field:ident : $fieldty:tt)),* $(,)* }) => { #[derive(Debug)] @@ -964,13 +964,13 @@ macro_rules! tlv_stream { #[cfg_attr(test, derive(PartialEq))] #[derive(Debug)] - pub(crate) struct $nameref<'a> { + pub(crate) struct $nameref<$($lifetime)*> { $( pub(super) $field: Option, )* } - impl<'a> $crate::util::ser::Writeable for $nameref<'a> { + impl<$($lifetime)*> $crate::util::ser::Writeable for $nameref<$($lifetime)*> { fn write(&self, writer: &mut W) -> Result<(), $crate::io::Error> { encode_tlv_stream!(writer, { $(($type, self.$field, (option, encoding: $fieldty))),*