From bbdd8738a8fd505d57def682528011b81eae8f6d Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Thu, 8 Aug 2024 19:11:27 -0500 Subject: [PATCH 01/19] Add parsing tests for unknown BOLT12 TLVs --- lightning/src/offers/invoice.rs | 82 ++++++++++++++++- lightning/src/offers/invoice_request.rs | 69 ++++++++++++++- lightning/src/offers/offer.rs | 41 ++++++++- lightning/src/offers/refund.rs | 41 ++++++++- lightning/src/offers/static_invoice.rs | 113 ++++++++++++++++++++++-- 5 files changed, 327 insertions(+), 19 deletions(-) diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index 648c0fba651..6cb37ab5339 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -1234,7 +1234,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, 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,7 +1248,7 @@ 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)), }); pub(super) type BlindedPathIter<'a> = core::iter::Map< @@ -1437,7 +1440,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, FallbackAddress, FullInvoiceTlvStreamRef, INVOICE_TYPES, InvoiceTlvStreamRef, SIGNATURE_TAG, UnsignedBolt12Invoice}; use bitcoin::{CompressedPublicKey, WitnessProgram, WitnessVersion}; use bitcoin::constants::ChainHash; @@ -2494,7 +2497,78 @@ 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(); + + BigSize(UNKNOWN_ODD_TYPE).write(&mut unsigned_invoice.bytes).unwrap(); + BigSize(32).write(&mut unsigned_invoice.bytes).unwrap(); + [42u8; 32].write(&mut unsigned_invoice.bytes).unwrap(); + + 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(); + + BigSize(UNKNOWN_EVEN_TYPE).write(&mut unsigned_invoice.bytes).unwrap(); + BigSize(32).write(&mut unsigned_invoice.bytes).unwrap(); + [42u8; 32].write(&mut unsigned_invoice.bytes).unwrap(); + + 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 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_request.rs b/lightning/src/offers/invoice_request.rs index c6c9da82a4e..e33ccb61606 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -1242,7 +1242,7 @@ impl Readable for InvoiceRequestFields { #[cfg(test)] mod tests { - use super::{InvoiceRequest, InvoiceRequestFields, InvoiceRequestTlvStreamRef, PAYER_NOTE_LIMIT, SIGNATURE_TAG, UnsignedInvoiceRequest}; + use super::{INVOICE_REQUEST_TYPES, InvoiceRequest, InvoiceRequestFields, InvoiceRequestTlvStreamRef, PAYER_NOTE_LIMIT, SIGNATURE_TAG, UnsignedInvoiceRequest}; use bitcoin::constants::ChainHash; use bitcoin::network::Network; @@ -2294,7 +2294,72 @@ 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(); + + BigSize(UNKNOWN_ODD_TYPE).write(&mut unsigned_invoice_request.bytes).unwrap(); + BigSize(32).write(&mut unsigned_invoice_request.bytes).unwrap(); + [42u8; 32].write(&mut unsigned_invoice_request.bytes).unwrap(); + + 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(); + + BigSize(UNKNOWN_EVEN_TYPE).write(&mut unsigned_invoice_request.bytes).unwrap(); + BigSize(32).write(&mut unsigned_invoice_request.bytes).unwrap(); + [42u8; 32].write(&mut unsigned_invoice_request.bytes).unwrap(); + + 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 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()) diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs index 8501b8d4651..546d42b8848 100644 --- a/lightning/src/offers/offer.rs +++ b/lightning/src/offers/offer.rs @@ -1173,7 +1173,7 @@ impl core::fmt::Display for Offer { #[cfg(test)] mod tests { - use super::{Amount, Offer, OfferTlvStreamRef, Quantity}; + use super::{Amount, OFFER_TYPES, Offer, OfferTlvStreamRef, Quantity}; #[cfg(not(c_bindings))] use { super::OfferBuilder, @@ -1860,12 +1860,47 @@ 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 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(80).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(); diff --git a/lightning/src/offers/refund.rs b/lightning/src/offers/refund.rs index 482c3b6884c..860de534ce0 100644 --- a/lightning/src/offers/refund.rs +++ b/lightning/src/offers/refund.rs @@ -944,7 +944,7 @@ mod tests { use crate::ln::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::{INVOICE_REQUEST_TYPES, InvoiceRequestTlvStreamRef}; use crate::offers::nonce::Nonce; use crate::offers::offer::OfferTlvStreamRef; use crate::offers::parse::{Bolt12ParseError, Bolt12SemanticError}; @@ -1522,7 +1522,44 @@ 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 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() diff --git a/lightning/src/offers/static_invoice.rs b/lightning/src/offers/static_invoice.rs index bf88bd9446c..dd3fb8b150b 100644 --- a/lightning/src/offers/static_invoice.rs +++ b/lightning/src/offers/static_invoice.rs @@ -130,10 +130,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 +148,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)) @@ -606,14 +612,15 @@ mod tests { use crate::ln::features::{Bolt12InvoiceFeatures, OfferFeatures}; use crate::ln::inbound_payment::ExpandedKey; use crate::ln::msgs::DecodeError; - use crate::offers::invoice::InvoiceTlvStreamRef; + use crate::offers::invoice::{InvoiceTlvStreamRef, INVOICE_TYPES}; use crate::offers::merkle; use crate::offers::merkle::{SignatureTlvStreamRef, TaggedHash}; use crate::offers::nonce::Nonce; use crate::offers::offer::{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; @@ -1185,7 +1192,97 @@ 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(); + + BigSize(UNKNOWN_ODD_TYPE).write(&mut unsigned_invoice.bytes).unwrap(); + BigSize(32).write(&mut unsigned_invoice.bytes).unwrap(); + [42u8; 32].write(&mut unsigned_invoice.bytes).unwrap(); + + 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(); + + BigSize(UNKNOWN_EVEN_TYPE).write(&mut unsigned_invoice.bytes).unwrap(); + BigSize(32).write(&mut unsigned_invoice.bytes).unwrap(); + [42u8; 32].write(&mut unsigned_invoice.bytes).unwrap(); + + 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 fails_parsing_invoice_with_out_of_range_tlv_records() { let invoice = invoice(); let mut encoded_invoice = Vec::new(); invoice.write(&mut encoded_invoice).unwrap(); From a77300129c2348698e667c88d10ace738733b45a Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Fri, 2 Aug 2024 11:54:42 -0500 Subject: [PATCH 02/19] Add optional lifetime to tlv_stream macro Using the tlv_stream macro without a type needing a reference results in a compilation error because of an unused lifetime parameter. To avoid this, add an optional lifetime parameter to the macro. This allows for experimental TLVs, which will be empty initially, and TLVs of entirely primitive types. --- lightning/src/offers/invoice.rs | 2 +- lightning/src/offers/invoice_request.rs | 2 +- lightning/src/offers/merkle.rs | 2 +- lightning/src/offers/offer.rs | 2 +- lightning/src/offers/payer.rs | 2 +- lightning/src/util/ser_macros.rs | 6 +++--- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index 6cb37ab5339..8fd9ab980fa 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -1237,7 +1237,7 @@ impl TryFrom> for Bolt12Invoice { /// Valid type range for invoice TLV records. pub(super) const INVOICE_TYPES: core::ops::Range = 160..240; -tlv_stream!(InvoiceTlvStream, InvoiceTlvStreamRef, INVOICE_TYPES, { +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)), diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index e33ccb61606..4d5c37bbaa9 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -1061,7 +1061,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)), diff --git a/lightning/src/offers/merkle.rs b/lightning/src/offers/merkle.rs index e2fed2e800b..15d6aababde 100644 --- a/lightning/src/offers/merkle.rs +++ b/lightning/src/offers/merkle.rs @@ -21,7 +21,7 @@ use crate::prelude::*; /// Valid type range for signature TLV records. const SIGNATURE_TYPES: core::ops::RangeInclusive = 240..=1000; -tlv_stream!(SignatureTlvStream, SignatureTlvStreamRef, SIGNATURE_TYPES, { +tlv_stream!(SignatureTlvStream, SignatureTlvStreamRef<'a>, SIGNATURE_TYPES, { (240, signature: Signature), }); diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs index 546d42b8848..4caa3757a34 100644 --- a/lightning/src/offers/offer.rs +++ b/lightning/src/offers/offer.rs @@ -1077,7 +1077,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), 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/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))),* From e23f89ce43d5b5311673726b737fda54f85b38fe Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Mon, 16 Sep 2024 14:42:06 -0500 Subject: [PATCH 03/19] Remove unneeded Eq and PartialEq derives TlvRecord has a few fields, but comparing only the record_bytes is sufficient for equality since the other fields are initialized from it. Remove the Eq and PartialEq derives as they compare these other fields. --- lightning/src/offers/merkle.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/lightning/src/offers/merkle.rs b/lightning/src/offers/merkle.rs index 15d6aababde..6cbad627909 100644 --- a/lightning/src/offers/merkle.rs +++ b/lightning/src/offers/merkle.rs @@ -249,7 +249,6 @@ impl<'a> TlvStream<'a> { } /// 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], From 771aa8ee2483dd0f3078dd0d704545ed90d68145 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Fri, 9 Aug 2024 18:36:24 -0500 Subject: [PATCH 04/19] Separate bytes for experimental TLVs When constructing UnsignedInvoiceRequest or UnsignedBolt12Invoice, use a separate field for experimental TLV bytes. This allows for properly inserting the signature TLVs before the experimental TLVs when signing. --- lightning/src/offers/invoice.rs | 109 ++++++++++++++++++++---- lightning/src/offers/invoice_request.rs | 104 ++++++++++++++++++---- lightning/src/offers/merkle.rs | 42 +++++---- lightning/src/offers/offer.rs | 3 + lightning/src/offers/static_invoice.rs | 77 ++++++++++++++--- 5 files changed, 264 insertions(+), 71 deletions(-) diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index 8fd9ab980fa..48d0ea1ba76 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, INVOICE_REQUEST_PAYER_ID_TYPE, INVOICE_REQUEST_TYPES, IV_BYTES as INVOICE_REQUEST_IV_BYTES, InvoiceRequest, InvoiceRequestContents, InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef}; +use crate::offers::merkle::{SignError, SignFn, SignatureTlvStream, SignatureTlvStreamRef, TaggedHash, TlvStream, self, SIGNATURE_TLV_RECORD_SIZE}; use crate::offers::nonce::Nonce; -use crate::offers::offer::{Amount, OFFER_TYPES, OfferTlvStream, OfferTlvStreamRef, Quantity}; +use crate::offers::offer::{Amount, EXPERIMENTAL_OFFER_TYPES, OFFER_TYPES, OfferTlvStream, OfferTlvStreamRef, Quantity}; use crate::offers::parse::{Bolt12ParseError, Bolt12SemanticError, ParsedMessage}; use crate::offers::payer::{PAYER_METADATA_TYPE, PayerTlvStream, PayerTlvStreamRef}; use crate::offers::refund::{IV_BYTES_WITH_METADATA as REFUND_IV_BYTES_WITH_METADATA, IV_BYTES_WITHOUT_METADATA as REFUND_IV_BYTES_WITHOUT_METADATA, Refund, RefundContents}; @@ -461,6 +461,7 @@ for InvoiceBuilder<'a, DerivedSigningPubkey> { #[derive(Clone)] pub struct UnsignedBolt12Invoice { bytes: Vec, + experimental_bytes: Vec, contents: InvoiceContents, tagged_hash: TaggedHash, } @@ -491,19 +492,57 @@ where impl UnsignedBolt12Invoice { fn new(invreq_bytes: &[u8], contents: InvoiceContents) -> Self { + // TLV record ranges applicable to invreq_bytes. + const NON_EXPERIMENTAL_TYPES: core::ops::Range = 0..INVOICE_REQUEST_TYPES.end; + const EXPERIMENTAL_TYPES: core::ops::Range = + EXPERIMENTAL_OFFER_TYPES.start..EXPERIMENTAL_INVOICE_REQUEST_TYPES.end; + + let (_, _, _, invoice_tlv_stream) = contents.as_tlv_stream(); + + // Allocate enough space for the invoice, which will include: + // - all TLV records from `invreq_bytes` except signatures, + // - all invoice-specific TLV records, and + // - a signature TLV record once the invoice is signed. + // + // This assumes both the invoice request and the invoice will each only have one signature + // using SIGNATURE_TYPES.start as the TLV record. Thus, it is accounted for by invreq_bytes. + let mut bytes = Vec::with_capacity( + invreq_bytes.len() + + invoice_tlv_stream.serialized_length() + + if contents.is_for_offer() { 0 } else { SIGNATURE_TLV_RECORD_SIZE } + ); + // Use the invoice_request bytes instead of the invoice_request TLV stream as the latter may // have contained unknown TLV records, which are not stored in `InvoiceRequestContents` or // `RefundContents`. - let (_, _, _, invoice_tlv_stream) = contents.as_tlv_stream(); - let invoice_request_bytes = WithoutSignatures(invreq_bytes); - let unsigned_tlv_stream = (invoice_request_bytes, invoice_tlv_stream); + for record in TlvStream::new(invreq_bytes).range(NON_EXPERIMENTAL_TYPES) { + record.write(&mut bytes).unwrap(); + } - let mut bytes = Vec::new(); - unsigned_tlv_stream.write(&mut bytes).unwrap(); + let remaining_bytes = &invreq_bytes[bytes.len()..]; - let tagged_hash = TaggedHash::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &bytes); + invoice_tlv_stream.write(&mut bytes).unwrap(); + + let mut experimental_tlv_stream = TlvStream::new(remaining_bytes) + .range(EXPERIMENTAL_TYPES) + .peekable(); + let mut experimental_bytes = Vec::with_capacity( + remaining_bytes.len() + - experimental_tlv_stream + .peek() + .map_or(remaining_bytes.len(), |first_record| first_record.start) + ); + + for record in experimental_tlv_stream { + record.write(&mut experimental_bytes).unwrap(); + } - Self { bytes, contents, tagged_hash } + debug_assert_eq!(experimental_bytes.len(), experimental_bytes.capacity()); + + let tlv_stream = TlvStream::new(&bytes).chain(TlvStream::new(&experimental_bytes)); + let tagged_hash = TaggedHash::from_tlv_stream(SIGNATURE_TAG, tlv_stream); + + Self { bytes, experimental_bytes, contents, tagged_hash } } /// Returns the [`TaggedHash`] of the invoice to sign. @@ -528,6 +567,17 @@ macro_rules! unsigned_invoice_sign_method { ($self: ident, $self_type: ty $(, $s }; signature_tlv_stream.write(&mut $self.bytes).unwrap(); + // Append the experimental bytes after the signature. + debug_assert_eq!( + // The two-byte overallocation results from SIGNATURE_TLV_RECORD_SIZE accommodating TLV + // records with types >= 253. + $self.bytes.len() + + $self.experimental_bytes.len() + + if $self.contents.is_for_offer() { 0 } else { 2 }, + $self.bytes.capacity(), + ); + $self.bytes.extend_from_slice(&$self.experimental_bytes); + Ok(Bolt12Invoice { #[cfg(not(c_bindings))] bytes: $self.bytes, @@ -882,6 +932,13 @@ impl Hash for Bolt12Invoice { } impl InvoiceContents { + fn is_for_offer(&self) -> bool { + match self { + InvoiceContents::ForOffer { .. } => true, + InvoiceContents::ForRefund { .. } => false, + } + } + /// Whether the original offer or refund has expired. #[cfg(feature = "std")] fn is_offer_or_refund_expired(&self) -> bool { @@ -1211,7 +1268,7 @@ impl TryFrom> for UnsignedBolt12Invoice { fn try_from(bytes: Vec) -> Result { let invoice = ParsedMessage::::try_from(bytes)?; - let ParsedMessage { bytes, tlv_stream } = invoice; + let ParsedMessage { mut bytes, tlv_stream } = invoice; let ( payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream, ) = tlv_stream; @@ -1221,7 +1278,13 @@ impl TryFrom> for UnsignedBolt12Invoice { let tagged_hash = TaggedHash::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &bytes); - Ok(UnsignedBolt12Invoice { bytes, contents, tagged_hash }) + let offset = TlvStream::new(&bytes) + .range(0..INVOICE_TYPES.end) + .last() + .map_or(0, |last_record| last_record.end); + let experimental_bytes = bytes.split_off(offset); + + Ok(UnsignedBolt12Invoice { bytes, experimental_bytes, contents, tagged_hash }) } } @@ -2512,10 +2575,15 @@ mod tests { .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() .build().unwrap(); - BigSize(UNKNOWN_ODD_TYPE).write(&mut unsigned_invoice.bytes).unwrap(); - BigSize(32).write(&mut unsigned_invoice.bytes).unwrap(); - [42u8; 32].write(&mut unsigned_invoice.bytes).unwrap(); + let mut unknown_bytes = Vec::new(); + BigSize(UNKNOWN_ODD_TYPE).write(&mut unknown_bytes).unwrap(); + BigSize(32).write(&mut unknown_bytes).unwrap(); + [42u8; 32].write(&mut unknown_bytes).unwrap(); + unsigned_invoice.bytes.reserve_exact( + unsigned_invoice.bytes.capacity() - unsigned_invoice.bytes.len() + unknown_bytes.len(), + ); + unsigned_invoice.bytes.extend_from_slice(&unknown_bytes); unsigned_invoice.tagged_hash = TaggedHash::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &unsigned_invoice.bytes); @@ -2545,10 +2613,15 @@ mod tests { .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() .build().unwrap(); - BigSize(UNKNOWN_EVEN_TYPE).write(&mut unsigned_invoice.bytes).unwrap(); - BigSize(32).write(&mut unsigned_invoice.bytes).unwrap(); - [42u8; 32].write(&mut unsigned_invoice.bytes).unwrap(); + let mut unknown_bytes = Vec::new(); + BigSize(UNKNOWN_EVEN_TYPE).write(&mut unknown_bytes).unwrap(); + BigSize(32).write(&mut unknown_bytes).unwrap(); + [42u8; 32].write(&mut unknown_bytes).unwrap(); + unsigned_invoice.bytes.reserve_exact( + unsigned_invoice.bytes.capacity() - unsigned_invoice.bytes.len() + unknown_bytes.len(), + ); + unsigned_invoice.bytes.extend_from_slice(&unknown_bytes); unsigned_invoice.tagged_hash = TaggedHash::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &unsigned_invoice.bytes); diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index 4d5c37bbaa9..5ec4ab8a7a2 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::ln::features::InvoiceRequestFeatures; use crate::ln::inbound_payment::{ExpandedKey, IV_LEN}; use crate::ln::msgs::DecodeError; -use crate::offers::merkle::{SignError, SignFn, SignatureTlvStream, SignatureTlvStreamRef, TaggedHash, self}; +use crate::offers::merkle::{SignError, SignFn, SignatureTlvStream, SignatureTlvStreamRef, TaggedHash, TlvStream, self, SIGNATURE_TLV_RECORD_SIZE}; use crate::offers::nonce::Nonce; -use crate::offers::offer::{Offer, OfferContents, OfferId, OfferTlvStream, OfferTlvStreamRef}; +use crate::offers::offer::{EXPERIMENTAL_OFFER_TYPES, OFFER_TYPES, Offer, OfferContents, OfferId, OfferTlvStream, OfferTlvStreamRef}; use crate::offers::parse::{Bolt12ParseError, ParsedMessage, Bolt12SemanticError}; use crate::offers::payer::{PayerContents, PayerTlvStream, PayerTlvStreamRef}; use crate::offers::signer::{Metadata, MetadataMaterial}; @@ -488,6 +488,7 @@ for InvoiceRequestBuilder<'a, 'b, DerivedPayerSigningPubkey, secp256k1::All> { #[derive(Clone)] pub struct UnsignedInvoiceRequest { bytes: Vec, + experimental_bytes: Vec, contents: InvoiceRequestContents, tagged_hash: TaggedHash, } @@ -520,17 +521,51 @@ impl UnsignedInvoiceRequest { fn new(offer: &Offer, contents: InvoiceRequestContents) -> Self { // Use the offer bytes instead of the offer TLV stream as the offer may have contained // unknown TLV records, which are not stored in `OfferContents`. - let (payer_tlv_stream, _offer_tlv_stream, invoice_request_tlv_stream) = - contents.as_tlv_stream(); - let offer_bytes = WithoutLength(&offer.bytes); - let unsigned_tlv_stream = (payer_tlv_stream, offer_bytes, invoice_request_tlv_stream); + let ( + payer_tlv_stream, _offer_tlv_stream, invoice_request_tlv_stream, + ) = contents.as_tlv_stream(); + + // Allocate enough space for the invoice_request, which will include: + // - all TLV records from `offer.bytes`, + // - all invoice_request-specific TLV records, and + // - a signature TLV record once the invoice_request is signed. + let mut bytes = Vec::with_capacity( + offer.bytes.len() + + payer_tlv_stream.serialized_length() + + invoice_request_tlv_stream.serialized_length() + + SIGNATURE_TLV_RECORD_SIZE + ); - let mut bytes = Vec::new(); - unsigned_tlv_stream.write(&mut bytes).unwrap(); + payer_tlv_stream.write(&mut bytes).unwrap(); - let tagged_hash = TaggedHash::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &bytes); + for record in TlvStream::new(&offer.bytes).range(OFFER_TYPES) { + record.write(&mut bytes).unwrap(); + } + + let remaining_bytes = &offer.bytes[bytes.len() - payer_tlv_stream.serialized_length()..]; + + invoice_request_tlv_stream.write(&mut bytes).unwrap(); + + let mut experimental_tlv_stream = TlvStream::new(remaining_bytes) + .range(EXPERIMENTAL_OFFER_TYPES) + .peekable(); + let mut experimental_bytes = Vec::with_capacity( + remaining_bytes.len() + - experimental_tlv_stream + .peek() + .map_or(remaining_bytes.len(), |first_record| first_record.start) + ); + + for record in experimental_tlv_stream { + record.write(&mut experimental_bytes).unwrap(); + } + + debug_assert_eq!(experimental_bytes.len(), experimental_bytes.capacity()); + + let tlv_stream = TlvStream::new(&bytes).chain(TlvStream::new(&experimental_bytes)); + let tagged_hash = TaggedHash::from_tlv_stream(SIGNATURE_TAG, tlv_stream); - Self { bytes, contents, tagged_hash } + Self { bytes, experimental_bytes, contents, tagged_hash } } /// Returns the [`TaggedHash`] of the invoice to sign. @@ -557,6 +592,15 @@ macro_rules! unsigned_invoice_request_sign_method { ( }; signature_tlv_stream.write(&mut $self.bytes).unwrap(); + // Append the experimental bytes after the signature. + debug_assert_eq!( + // The two-byte overallocation results from SIGNATURE_TLV_RECORD_SIZE accommodating TLV + // records with types >= 253. + $self.bytes.len() + $self.experimental_bytes.len() + 2, + $self.bytes.capacity(), + ); + $self.bytes.extend_from_slice(&$self.experimental_bytes); + Ok(InvoiceRequest { #[cfg(not(c_bindings))] bytes: $self.bytes, @@ -1072,6 +1116,10 @@ tlv_stream!(InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef<'a>, INVOICE_REQ (90, paths: (Vec, WithoutLength)), }); +/// 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; + type FullInvoiceRequestTlvStream = (PayerTlvStream, OfferTlvStream, InvoiceRequestTlvStream, SignatureTlvStream); @@ -1106,7 +1154,7 @@ 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 ParsedMessage { mut bytes, tlv_stream } = invoice_request; let ( payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, ) = tlv_stream; @@ -1116,7 +1164,13 @@ impl TryFrom> for UnsignedInvoiceRequest { let tagged_hash = TaggedHash::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &bytes); - Ok(UnsignedInvoiceRequest { bytes, contents, tagged_hash }) + let offset = TlvStream::new(&bytes) + .range(0..INVOICE_REQUEST_TYPES.end) + .last() + .map_or(0, |last_record| last_record.end); + let experimental_bytes = bytes.split_off(offset); + + Ok(UnsignedInvoiceRequest { bytes, experimental_bytes, contents, tagged_hash }) } } @@ -2306,10 +2360,17 @@ mod tests { .request_invoice(vec![1; 32], keys.public_key()).unwrap() .build().unwrap(); - BigSize(UNKNOWN_ODD_TYPE).write(&mut unsigned_invoice_request.bytes).unwrap(); - BigSize(32).write(&mut unsigned_invoice_request.bytes).unwrap(); - [42u8; 32].write(&mut unsigned_invoice_request.bytes).unwrap(); + let mut unknown_bytes = Vec::new(); + BigSize(UNKNOWN_ODD_TYPE).write(&mut unknown_bytes).unwrap(); + BigSize(32).write(&mut unknown_bytes).unwrap(); + [42u8; 32].write(&mut unknown_bytes).unwrap(); + unsigned_invoice_request.bytes.reserve_exact( + unsigned_invoice_request.bytes.capacity() + - unsigned_invoice_request.bytes.len() + + unknown_bytes.len(), + ); + unsigned_invoice_request.bytes.extend_from_slice(&unknown_bytes); unsigned_invoice_request.tagged_hash = TaggedHash::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &unsigned_invoice_request.bytes); @@ -2336,10 +2397,17 @@ mod tests { .request_invoice(vec![1; 32], keys.public_key()).unwrap() .build().unwrap(); - BigSize(UNKNOWN_EVEN_TYPE).write(&mut unsigned_invoice_request.bytes).unwrap(); - BigSize(32).write(&mut unsigned_invoice_request.bytes).unwrap(); - [42u8; 32].write(&mut unsigned_invoice_request.bytes).unwrap(); + let mut unknown_bytes = Vec::new(); + BigSize(UNKNOWN_EVEN_TYPE).write(&mut unknown_bytes).unwrap(); + BigSize(32).write(&mut unknown_bytes).unwrap(); + [42u8; 32].write(&mut unknown_bytes).unwrap(); + unsigned_invoice_request.bytes.reserve_exact( + unsigned_invoice_request.bytes.capacity() + - unsigned_invoice_request.bytes.len() + + unknown_bytes.len(), + ); + unsigned_invoice_request.bytes.extend_from_slice(&unknown_bytes); unsigned_invoice_request.tagged_hash = TaggedHash::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &unsigned_invoice_request.bytes); diff --git a/lightning/src/offers/merkle.rs b/lightning/src/offers/merkle.rs index 6cbad627909..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<'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,12 +245,6 @@ impl<'a> TlvStream<'a> { self.skip_while(move |record| !types.contains(&record.r#type)) .take_while(move |record| take_range.contains(&record.r#type)) } - - fn skip_signatures( - tlv_stream: impl core::iter::Iterator> - ) -> impl core::iter::Iterator> { - tlv_stream.filter(|record| !SIGNATURE_TYPES.contains(&record.r#type)) - } } /// A slice into a [`TlvStream`] for a record. @@ -254,6 +253,8 @@ pub(super) struct TlvRecord<'a> { type_bytes: &'a [u8], // The entire TLV record. pub(super) record_bytes: &'a [u8], + pub(super) start: usize, + pub(super) end: usize, } impl<'a> Iterator for TlvStream<'a> { @@ -276,32 +277,25 @@ impl<'a> Iterator for TlvStream<'a> { self.data.set_position(end); - Some(TlvRecord { r#type, type_bytes, record_bytes }) + Some(TlvRecord { + r#type, type_bytes, record_bytes, start: start as usize, end: end as usize, + }) } else { None } } } -/// Encoding for a pre-serialized TLV stream that excludes any signature TLV records. -/// -/// Panics if the wrapped bytes are not a well-formed TLV stream. -pub(super) struct WithoutSignatures<'a>(pub &'a [u8]); - -impl<'a> Writeable for WithoutSignatures<'a> { +impl<'a> Writeable for TlvRecord<'a> { #[inline] fn write(&self, writer: &mut W) -> Result<(), io::Error> { - let tlv_stream = TlvStream::new(self.0); - for record in TlvStream::skip_signatures(tlv_stream) { - writer.write_all(record.record_bytes)?; - } - Ok(()) + writer.write_all(self.record_bytes) } } #[cfg(test)] mod tests { - use super::{SIGNATURE_TYPES, TlvStream, WithoutSignatures}; + use super::{SIGNATURE_TYPES, TlvStream}; use bitcoin::hashes::{Hash, sha256}; use bitcoin::hex::FromHex; @@ -411,7 +405,11 @@ mod tests { .unwrap(); let mut bytes_without_signature = Vec::new(); - WithoutSignatures(&invoice_request.bytes).write(&mut bytes_without_signature).unwrap(); + let tlv_stream_without_signatures = TlvStream::new(&invoice_request.bytes) + .filter(|record| !SIGNATURE_TYPES.contains(&record.r#type)); + for record in tlv_stream_without_signatures { + record.write(&mut bytes_without_signature).unwrap(); + } assert_ne!(bytes_without_signature, invoice_request.bytes); assert_eq!( diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs index 4caa3757a34..675395841cd 100644 --- a/lightning/src/offers/offer.rs +++ b/lightning/src/offers/offer.rs @@ -1091,6 +1091,9 @@ tlv_stream!(OfferTlvStream, OfferTlvStreamRef<'a>, OFFER_TYPES, { (OFFER_ISSUER_ID_TYPE, issuer_id: PublicKey), }); +/// Valid type range for experimental offer TLV records. +pub(super) const EXPERIMENTAL_OFFER_TYPES: core::ops::Range = 1_000_000_000..2_000_000_000; + impl Bech32Encode for Offer { const BECH32_HRP: &'static str = "lno"; } diff --git a/lightning/src/offers/static_invoice.rs b/lightning/src/offers/static_invoice.rs index dd3fb8b150b..07f2b9d3152 100644 --- a/lightning/src/offers/static_invoice.rs +++ b/lightning/src/offers/static_invoice.rs @@ -23,10 +23,12 @@ use crate::offers::invoice_macros::{invoice_accessors_common, invoice_builder_me use crate::offers::invoice_request::InvoiceRequest; use crate::offers::merkle::{ self, SignError, SignFn, SignatureTlvStream, SignatureTlvStreamRef, TaggedHash, TlvStream, + SIGNATURE_TLV_RECORD_SIZE, }; use crate::offers::nonce::Nonce; use crate::offers::offer::{ - Amount, Offer, OfferContents, OfferTlvStream, OfferTlvStreamRef, Quantity, OFFER_TYPES, + Amount, Offer, OfferContents, OfferTlvStream, OfferTlvStreamRef, Quantity, + EXPERIMENTAL_OFFER_TYPES, OFFER_TYPES, }; use crate::offers::parse::{Bolt12ParseError, Bolt12SemanticError, ParsedMessage}; use crate::util::ser::{CursorReadable, Iterable, WithoutLength, Writeable, Writer}; @@ -170,6 +172,7 @@ impl<'a> StaticInvoiceBuilder<'a> { /// A semantically valid [`StaticInvoice`] that hasn't been signed. pub struct UnsignedStaticInvoice { bytes: Vec, + experimental_bytes: Vec, contents: InvoiceContents, tagged_hash: TaggedHash, } @@ -276,15 +279,44 @@ 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 mut bytes = Vec::new(); - unsigned_tlv_stream.write(&mut bytes).unwrap(); + // Allocate enough space for the invoice, which will include: + // - all TLV records from `offer_bytes`, + // - all invoice-specific TLV records, and + // - a signature TLV record once the invoice is signed. + let mut bytes = Vec::with_capacity( + offer_bytes.len() + invoice_tlv_stream.serialized_length() + SIGNATURE_TLV_RECORD_SIZE, + ); - let tagged_hash = TaggedHash::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &bytes); + // Use the offer bytes instead of the offer TLV stream as the latter may have contained + // unknown TLV records, which are not stored in `InvoiceContents`. + for record in TlvStream::new(offer_bytes).range(OFFER_TYPES) { + record.write(&mut bytes).unwrap(); + } + + let remaining_bytes = &offer_bytes[bytes.len()..]; - Self { contents, tagged_hash, bytes } + invoice_tlv_stream.write(&mut bytes).unwrap(); + + let mut experimental_tlv_stream = + TlvStream::new(remaining_bytes).range(EXPERIMENTAL_OFFER_TYPES).peekable(); + let mut experimental_bytes = Vec::with_capacity( + remaining_bytes.len() + - experimental_tlv_stream + .peek() + .map_or(remaining_bytes.len(), |first_record| first_record.start), + ); + + for record in experimental_tlv_stream { + record.write(&mut experimental_bytes).unwrap(); + } + + debug_assert_eq!(experimental_bytes.len(), experimental_bytes.capacity()); + + let tlv_stream = TlvStream::new(&bytes).chain(TlvStream::new(&experimental_bytes)); + let tagged_hash = TaggedHash::from_tlv_stream(SIGNATURE_TAG, tlv_stream); + + Self { bytes, experimental_bytes, contents, tagged_hash } } /// Signs the [`TaggedHash`] of the invoice using the given function. @@ -298,6 +330,15 @@ impl UnsignedStaticInvoice { let signature_tlv_stream = SignatureTlvStreamRef { signature: Some(&signature) }; signature_tlv_stream.write(&mut self.bytes).unwrap(); + // Append the experimental bytes after the signature. + debug_assert_eq!( + // The two-byte overallocation results from SIGNATURE_TLV_RECORD_SIZE accommodating TLV + // records with types >= 253. + self.bytes.len() + self.experimental_bytes.len() + 2, + self.bytes.capacity(), + ); + self.bytes.extend_from_slice(&self.experimental_bytes); + Ok(StaticInvoice { bytes: self.bytes, contents: self.contents, signature }) } @@ -1222,10 +1263,15 @@ mod tests { .build() .unwrap(); - BigSize(UNKNOWN_ODD_TYPE).write(&mut unsigned_invoice.bytes).unwrap(); - BigSize(32).write(&mut unsigned_invoice.bytes).unwrap(); - [42u8; 32].write(&mut unsigned_invoice.bytes).unwrap(); + let mut unknown_bytes = Vec::new(); + BigSize(UNKNOWN_ODD_TYPE).write(&mut unknown_bytes).unwrap(); + BigSize(32).write(&mut unknown_bytes).unwrap(); + [42u8; 32].write(&mut unknown_bytes).unwrap(); + unsigned_invoice.bytes.reserve_exact( + unsigned_invoice.bytes.capacity() - unsigned_invoice.bytes.len() + unknown_bytes.len(), + ); + unsigned_invoice.bytes.extend_from_slice(&unknown_bytes); unsigned_invoice.tagged_hash = TaggedHash::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &unsigned_invoice.bytes); @@ -1259,10 +1305,15 @@ mod tests { .build() .unwrap(); - BigSize(UNKNOWN_EVEN_TYPE).write(&mut unsigned_invoice.bytes).unwrap(); - BigSize(32).write(&mut unsigned_invoice.bytes).unwrap(); - [42u8; 32].write(&mut unsigned_invoice.bytes).unwrap(); + let mut unknown_bytes = Vec::new(); + BigSize(UNKNOWN_EVEN_TYPE).write(&mut unknown_bytes).unwrap(); + BigSize(32).write(&mut unknown_bytes).unwrap(); + [42u8; 32].write(&mut unknown_bytes).unwrap(); + unsigned_invoice.bytes.reserve_exact( + unsigned_invoice.bytes.capacity() - unsigned_invoice.bytes.len() + unknown_bytes.len(), + ); + unsigned_invoice.bytes.extend_from_slice(&unknown_bytes); unsigned_invoice.tagged_hash = TaggedHash::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &unsigned_invoice.bytes); From 1eb5baa00c081a0afe1b87f6c3d58bbb080ceba6 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Mon, 16 Sep 2024 16:07:54 -0500 Subject: [PATCH 05/19] Function for iterating over Offer TLV records Add a utility function for iterating over Offer TLV records contained in any valid TLV stream bytes. Using a common function ensures that experimental TLV records are included once they are supported. --- lightning/src/offers/offer.rs | 10 ++++++++-- lightning/src/offers/static_invoice.rs | 10 ++++------ 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs index 675395841cd..c33d6510fdd 100644 --- a/lightning/src/offers/offer.rs +++ b/lightning/src/offers/offer.rs @@ -90,7 +90,7 @@ use crate::ln::channelmanager::PaymentId; use crate::ln::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}; @@ -128,7 +128,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()) } @@ -687,6 +687,12 @@ 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) + } + #[cfg(async_payments)] pub(super) fn verify( &self, nonce: Nonce, key: &ExpandedKey, secp_ctx: &Secp256k1 diff --git a/lightning/src/offers/static_invoice.rs b/lightning/src/offers/static_invoice.rs index 07f2b9d3152..b82a369bad3 100644 --- a/lightning/src/offers/static_invoice.rs +++ b/lightning/src/offers/static_invoice.rs @@ -388,12 +388,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) } } From 63c687f0d9e6480877d7decedf8c977df41de3dd Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Mon, 16 Sep 2024 16:31:01 -0500 Subject: [PATCH 06/19] Pass bytes instead of TlvStream to verify Passing bytes directly to InvoiceContents::verify improves readability as then a TlvStream for each TLV record range can be created from the bytes instead of needing to clone the TlvStream upfront. In an upcoming commit, the experimental TLV record range will utilize this. --- lightning/src/offers/invoice.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index 48d0ea1ba76..894c45d42f2 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -878,7 +878,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 @@ -892,7 +892,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(()) @@ -1143,11 +1144,11 @@ 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| { + 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(), From bcc7c1dc1d8e08c93b3f94db509ee4a0ed692608 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Thu, 17 Oct 2024 17:51:54 -0500 Subject: [PATCH 07/19] Include experimental TLV records when verifying Upcoming commits will allow parsing BOLT12 messages that include TLV records in the experimental range. Include these ranges when verifying messages since they will be included in the message bytes. --- lightning/src/offers/invoice.rs | 6 +++++- lightning/src/offers/offer.rs | 4 +++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index 894c45d42f2..92b75baafbf 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -1147,6 +1147,9 @@ impl InvoiceContents { &self, bytes: &[u8], metadata: &Metadata, key: &ExpandedKey, iv_bytes: &[u8; IV_LEN], secp_ctx: &Secp256k1 ) -> Result { + 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 { @@ -1155,7 +1158,8 @@ impl InvoiceContents { _ => 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( diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs index c33d6510fdd..81078726591 100644 --- a/lightning/src/offers/offer.rs +++ b/lightning/src/offers/offer.rs @@ -963,7 +963,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(()), From 38e9457d97f005d0044d94ae72ede2beed51b918 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Mon, 21 Oct 2024 12:15:24 -0500 Subject: [PATCH 08/19] Remove unnecessary destructure --- lightning/src/offers/invoice.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index 92b75baafbf..0dfe49cec9f 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -1274,12 +1274,7 @@ impl TryFrom> for UnsignedBolt12Invoice { fn try_from(bytes: Vec) -> Result { let invoice = ParsedMessage::::try_from(bytes)?; let ParsedMessage { mut 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 contents = InvoiceContents::try_from(tlv_stream)?; let tagged_hash = TaggedHash::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &bytes); From e01e731ff7d1bfc62531fb333ac03ccf399c8d9a Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Mon, 5 Aug 2024 18:51:32 -0500 Subject: [PATCH 09/19] Parse experimental offer TLV records The BOLT12 spec defines an experimental TLV range that are allowed in offer messages. Allow this range when parsing an offer and include those bytes in any invoice requests. Also include those bytes when computing an OfferId and verifying that an InvoiceRequest is for a valid Offer. --- lightning/src/offers/invoice.rs | 75 +++++++---- lightning/src/offers/invoice_request.rs | 112 ++++++++++------ lightning/src/offers/offer.rs | 167 ++++++++++++++---------- lightning/src/offers/refund.rs | 42 +++--- lightning/src/offers/static_invoice.rs | 66 +++++++--- lightning/src/util/ser.rs | 20 +++ 6 files changed, 309 insertions(+), 173 deletions(-) diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index 0dfe49cec9f..c8be0e3771e 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -124,7 +124,7 @@ use crate::offers::invoice_macros::invoice_builder_methods_test; use crate::offers::invoice_request::{EXPERIMENTAL_INVOICE_REQUEST_TYPES, INVOICE_REQUEST_PAYER_ID_TYPE, INVOICE_REQUEST_TYPES, IV_BYTES as INVOICE_REQUEST_IV_BYTES, InvoiceRequest, InvoiceRequestContents, InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef}; use crate::offers::merkle::{SignError, SignFn, SignatureTlvStream, SignatureTlvStreamRef, TaggedHash, TlvStream, self, SIGNATURE_TLV_RECORD_SIZE}; use crate::offers::nonce::Nonce; -use crate::offers::offer::{Amount, EXPERIMENTAL_OFFER_TYPES, 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}; @@ -497,7 +497,7 @@ impl UnsignedBolt12Invoice { const EXPERIMENTAL_TYPES: core::ops::Range = EXPERIMENTAL_OFFER_TYPES.start..EXPERIMENTAL_INVOICE_REQUEST_TYPES.end; - let (_, _, _, invoice_tlv_stream) = contents.as_tlv_stream(); + let (_, _, _, invoice_tlv_stream, _) = contents.as_tlv_stream(); // Allocate enough space for the invoice, which will include: // - all TLV records from `invreq_bytes` except signatures, @@ -901,13 +901,17 @@ 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, + ) = 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, + ) } pub(crate) fn is_for_refund_without_paths(&self) -> bool { @@ -1145,7 +1149,7 @@ impl InvoiceContents { fn verify( &self, bytes: &[u8], metadata: &Metadata, key: &ExpandedKey, iv_bytes: &[u8; IV_LEN], - secp_ctx: &Secp256k1 + secp_ctx: &Secp256k1, ) -> Result { const EXPERIMENTAL_TYPES: core::ops::Range = EXPERIMENTAL_OFFER_TYPES.start..EXPERIMENTAL_INVOICE_REQUEST_TYPES.end; @@ -1168,13 +1172,13 @@ impl InvoiceContents { } fn as_tlv_stream(&self) -> PartialInvoiceTlvStreamRef { - let (payer, offer, invoice_request) = match self { + let (payer, offer, invoice_request, experimental_offer) = 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(); - (payer, offer, invoice_request, invoice) + (payer, offer, invoice_request, invoice, experimental_offer) } } @@ -1333,8 +1337,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, +); type FullInvoiceTlvStreamRef<'a> = ( PayerTlvStreamRef<'a>, @@ -1342,6 +1348,7 @@ type FullInvoiceTlvStreamRef<'a> = ( InvoiceRequestTlvStreamRef<'a>, InvoiceTlvStreamRef<'a>, SignatureTlvStreamRef<'a>, + ExperimentalOfferTlvStreamRef, ); impl CursorReadable for FullInvoiceTlvStream { @@ -1351,19 +1358,23 @@ 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)?; - Ok((payer, offer, invoice_request, invoice, signature)) + Ok((payer, offer, invoice_request, invoice, signature, experimental_offer)) } } -type PartialInvoiceTlvStream = - (PayerTlvStream, OfferTlvStream, InvoiceRequestTlvStream, InvoiceTlvStream); +type PartialInvoiceTlvStream = ( + PayerTlvStream, OfferTlvStream, InvoiceRequestTlvStream, InvoiceTlvStream, + ExperimentalOfferTlvStream, +); type PartialInvoiceTlvStreamRef<'a> = ( PayerTlvStreamRef<'a>, OfferTlvStreamRef<'a>, InvoiceRequestTlvStreamRef<'a>, InvoiceTlvStreamRef<'a>, + ExperimentalOfferTlvStreamRef, ); impl CursorReadable for PartialInvoiceTlvStream { @@ -1372,8 +1383,9 @@ 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)?; - Ok((payer, offer, invoice_request, invoice)) + Ok((payer, offer, invoice_request, invoice, experimental_offer)) } } @@ -1385,9 +1397,13 @@ impl TryFrom> for Bolt12Invoice { let ( payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream, SignatureTlvStream { signature }, + experimental_offer_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, + ) )?; let signature = signature.ok_or( @@ -1413,6 +1429,7 @@ impl TryFrom for InvoiceContents { paths, blindedpay, created_at, relative_expiry, payment_hash, amount, fallbacks, features, node_id, message_paths, }, + experimental_offer_tlv_stream, ) = tlv_stream; if message_paths.is_some() { return Err(Bolt12SemanticError::UnexpectedPaths) } @@ -1445,12 +1462,18 @@ impl TryFrom for InvoiceContents { 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, + ) )?; 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, + ) )?; Ok(InvoiceContents::ForOffer { invoice_request, fields }) } @@ -1525,7 +1548,7 @@ mod tests { use crate::offers::invoice_request::InvoiceRequestTlvStreamRef; use crate::offers::merkle::{SignError, SignatureTlvStreamRef, TaggedHash, 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 { @@ -1693,6 +1716,7 @@ mod tests { message_paths: None, }, SignatureTlvStreamRef { signature: Some(&invoice.signature()) }, + ExperimentalOfferTlvStreamRef {}, ), ); @@ -1786,6 +1810,7 @@ mod tests { message_paths: None, }, SignatureTlvStreamRef { signature: Some(&invoice.signature()) }, + ExperimentalOfferTlvStreamRef {}, ), ); @@ -1979,7 +2004,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); @@ -1995,7 +2020,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)); @@ -2014,7 +2039,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)); } @@ -2032,7 +2057,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)); @@ -2070,7 +2095,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![ @@ -2113,7 +2138,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)); } diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index 5ec4ab8a7a2..231c582d599 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -71,7 +71,7 @@ use crate::ln::inbound_payment::{ExpandedKey, IV_LEN}; use crate::ln::msgs::DecodeError; use crate::offers::merkle::{SignError, SignFn, SignatureTlvStream, SignatureTlvStreamRef, TaggedHash, TlvStream, self, SIGNATURE_TLV_RECORD_SIZE}; use crate::offers::nonce::Nonce; -use crate::offers::offer::{EXPERIMENTAL_OFFER_TYPES, OFFER_TYPES, 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}; @@ -523,6 +523,7 @@ impl UnsignedInvoiceRequest { // unknown TLV records, which are not stored in `OfferContents`. let ( payer_tlv_stream, _offer_tlv_stream, invoice_request_tlv_stream, + _experimental_offer_tlv_stream, ) = contents.as_tlv_stream(); // Allocate enough space for the invoice_request, which will include: @@ -907,12 +908,17 @@ 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, + ) = 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, + ) } } @@ -1028,9 +1034,9 @@ 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) = self.inner.as_tlv_stream(); invoice_request.payer_id = Some(&self.payer_signing_pubkey); - (payer, offer, invoice_request) + (payer, offer, invoice_request, experimental_offer) } } @@ -1048,7 +1054,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 } @@ -1065,7 +1071,7 @@ impl InvoiceRequestContentsWithoutPayerSigningPubkey { paths: None, }; - (payer, offer, invoice_request) + (payer, offer, invoice_request, experimental_offer) } } @@ -1120,14 +1126,17 @@ tlv_stream!(InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef<'a>, INVOICE_REQ pub(super) const EXPERIMENTAL_INVOICE_REQUEST_TYPES: core::ops::Range = 2_000_000_000..3_000_000_000; -type FullInvoiceRequestTlvStream = - (PayerTlvStream, OfferTlvStream, InvoiceRequestTlvStream, SignatureTlvStream); +type FullInvoiceRequestTlvStream = ( + PayerTlvStream, OfferTlvStream, InvoiceRequestTlvStream, SignatureTlvStream, + ExperimentalOfferTlvStream, +); type FullInvoiceRequestTlvStreamRef<'a> = ( PayerTlvStreamRef<'a>, OfferTlvStreamRef<'a>, InvoiceRequestTlvStreamRef<'a>, SignatureTlvStreamRef<'a>, + ExperimentalOfferTlvStreamRef, ); impl CursorReadable for FullInvoiceRequestTlvStream { @@ -1136,17 +1145,21 @@ 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)?; - Ok((payer, offer, invoice_request, signature)) + Ok((payer, offer, invoice_request, signature, experimental_offer)) } } -type PartialInvoiceRequestTlvStream = (PayerTlvStream, OfferTlvStream, InvoiceRequestTlvStream); +type PartialInvoiceRequestTlvStream = ( + PayerTlvStream, OfferTlvStream, InvoiceRequestTlvStream, ExperimentalOfferTlvStream, +); type PartialInvoiceRequestTlvStreamRef<'a> = ( PayerTlvStreamRef<'a>, OfferTlvStreamRef<'a>, InvoiceRequestTlvStreamRef<'a>, + ExperimentalOfferTlvStreamRef, ); impl TryFrom> for UnsignedInvoiceRequest { @@ -1155,13 +1168,8 @@ impl TryFrom> for UnsignedInvoiceRequest { fn try_from(bytes: Vec) -> Result { let invoice_request = ParsedMessage::::try_from(bytes)?; let ParsedMessage { mut 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 contents = InvoiceRequestContents::try_from(tlv_stream)?; let tagged_hash = TaggedHash::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &bytes); let offset = TlvStream::new(&bytes) @@ -1183,9 +1191,13 @@ impl TryFrom> for InvoiceRequest { let ( payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, SignatureTlvStream { signature }, + experimental_offer_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, + ) )?; let signature = match signature { @@ -1209,13 +1221,14 @@ impl TryFrom for InvoiceRequestContents { InvoiceRequestTlvStream { chain, amount, features, quantity, payer_id, payer_note, paths, }, + experimental_offer_tlv_stream, ) = 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); @@ -1312,7 +1325,7 @@ mod tests { use crate::offers::invoice::{Bolt12Invoice, SIGNATURE_TAG as INVOICE_SIGNATURE_TAG}; use crate::offers::merkle::{SignError, SignatureTlvStreamRef, TaggedHash, 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, @@ -1421,6 +1434,7 @@ mod tests { paths: None, }, SignatureTlvStreamRef { signature: Some(&invoice_request.signature()) }, + ExperimentalOfferTlvStreamRef {}, ), ); @@ -1491,7 +1505,7 @@ 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, ) = invoice.as_tlv_stream(); invoice_request_tlv_stream.amount = Some(2000); invoice_tlv_stream.amount = Some(2000); @@ -1500,13 +1514,16 @@ mod tests { (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream); let mut bytes = Vec::new(); tlv_stream.write(&mut bytes).unwrap(); + experimental_offer_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; + let mut encoded_invoice = Vec::new(); + tlv_stream.write(&mut encoded_invoice).unwrap(); signature_tlv_stream.write(&mut encoded_invoice).unwrap(); + experimental_offer_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()); @@ -1514,7 +1531,7 @@ 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, ) = invoice.as_tlv_stream(); let metadata = payer_tlv_stream.metadata.unwrap().iter().copied().rev().collect(); payer_tlv_stream.metadata = Some(&metadata); @@ -1523,13 +1540,16 @@ mod tests { (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream); let mut bytes = Vec::new(); tlv_stream.write(&mut bytes).unwrap(); + experimental_offer_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; + let mut encoded_invoice = Vec::new(); + tlv_stream.write(&mut encoded_invoice).unwrap(); signature_tlv_stream.write(&mut encoded_invoice).unwrap(); + experimental_offer_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()); @@ -1564,7 +1584,7 @@ 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, ) = invoice.as_tlv_stream(); invoice_request_tlv_stream.amount = Some(2000); invoice_tlv_stream.amount = Some(2000); @@ -1573,13 +1593,16 @@ mod tests { (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream); let mut bytes = Vec::new(); tlv_stream.write(&mut bytes).unwrap(); + experimental_offer_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; + let mut encoded_invoice = Vec::new(); + tlv_stream.write(&mut encoded_invoice).unwrap(); signature_tlv_stream.write(&mut encoded_invoice).unwrap(); + experimental_offer_tlv_stream.write(&mut encoded_invoice).unwrap(); let invoice = Bolt12Invoice::try_from(encoded_invoice).unwrap(); assert!( @@ -1589,7 +1612,7 @@ 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, ) = invoice.as_tlv_stream(); let payer_id = pubkey(1); invoice_request_tlv_stream.payer_id = Some(&payer_id); @@ -1598,13 +1621,16 @@ mod tests { (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream); let mut bytes = Vec::new(); tlv_stream.write(&mut bytes).unwrap(); + experimental_offer_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; + let mut encoded_invoice = Vec::new(); + tlv_stream.write(&mut encoded_invoice).unwrap(); signature_tlv_stream.write(&mut encoded_invoice).unwrap(); + experimental_offer_tlv_stream.write(&mut encoded_invoice).unwrap(); let invoice = Bolt12Invoice::try_from(encoded_invoice).unwrap(); assert!( @@ -1624,7 +1650,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); @@ -1636,7 +1662,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)); @@ -1649,7 +1675,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); @@ -1663,7 +1689,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)); @@ -1699,7 +1725,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)); @@ -1711,7 +1737,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)); @@ -1722,7 +1748,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)); @@ -1802,7 +1828,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())); @@ -1814,7 +1840,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); } @@ -1831,7 +1857,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); @@ -1856,7 +1882,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)); @@ -1881,7 +1907,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)); @@ -1917,7 +1943,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"))); @@ -1929,7 +1955,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"))); } diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs index 81078726591..1c7eac6167e 100644 --- a/lightning/src/offers/offer.rs +++ b/lightning/src/offers/offer.rs @@ -94,7 +94,7 @@ 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))] @@ -400,10 +400,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 @@ -691,6 +691,7 @@ impl Offer { bytes: &'a [u8] ) -> impl core::iter::Iterator> { TlvStream::new(bytes).range(OFFER_TYPES) + .chain(TlvStream::new(bytes).range(EXPERIMENTAL_OFFER_TYPES)) } #[cfg(async_payments)] @@ -795,7 +796,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() } } @@ -982,7 +983,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)), @@ -995,7 +996,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, @@ -1007,7 +1008,11 @@ 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 {}; + + (offer, experimental_offer) } } @@ -1102,6 +1107,22 @@ tlv_stream!(OfferTlvStream, OfferTlvStreamRef<'a>, OFFER_TYPES, { /// 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; +tlv_stream!(ExperimentalOfferTlvStream, ExperimentalOfferTlvStreamRef, EXPERIMENTAL_OFFER_TYPES, { +}); + +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"; } @@ -1118,7 +1139,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); @@ -1127,14 +1148,17 @@ 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 {}, + ) = tlv_stream; let metadata = metadata.map(|metadata| Metadata::Bytes(metadata)); @@ -1184,7 +1208,7 @@ impl core::fmt::Display for Offer { #[cfg(test)] mod tests { - use super::{Amount, OFFER_TYPES, Offer, OfferTlvStreamRef, Quantity}; + use super::{Amount, ExperimentalOfferTlvStreamRef, OFFER_TYPES, Offer, OfferTlvStreamRef, Quantity}; #[cfg(not(c_bindings))] use { super::OfferBuilder, @@ -1236,19 +1260,22 @@ 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 {}, + ), ); if let Err(e) = Offer::try_from(buffer) { @@ -1267,7 +1294,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) @@ -1275,7 +1302,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) @@ -1284,7 +1311,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) @@ -1294,7 +1321,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] @@ -1304,7 +1331,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() @@ -1312,7 +1339,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] @@ -1349,7 +1376,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(); @@ -1362,8 +1389,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(); @@ -1416,7 +1443,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(); @@ -1432,7 +1459,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(); @@ -1457,8 +1484,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)) @@ -1469,8 +1496,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), @@ -1482,8 +1509,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() { @@ -1499,7 +1526,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()) @@ -1507,14 +1534,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] @@ -1524,7 +1551,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()) @@ -1532,7 +1559,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] @@ -1549,7 +1576,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) @@ -1560,7 +1587,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] @@ -1591,8 +1618,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] @@ -1602,7 +1629,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()) @@ -1610,7 +1637,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] @@ -1625,7 +1652,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) @@ -1634,7 +1661,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)) @@ -1643,7 +1670,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)) @@ -1652,7 +1679,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)) @@ -1662,7 +1689,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] @@ -1700,8 +1727,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(); @@ -1711,8 +1738,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(); @@ -1723,8 +1750,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(); @@ -1751,7 +1778,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(); @@ -1857,7 +1884,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(); diff --git a/lightning/src/offers/refund.rs b/lightning/src/offers/refund.rs index 860de534ce0..8808c7c5002 100644 --- a/lightning/src/offers/refund.rs +++ b/lightning/src/offers/refund.rs @@ -100,7 +100,7 @@ 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::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}; @@ -770,7 +770,9 @@ impl RefundContents { paths: self.paths.as_ref(), }; - (payer, offer, invoice_request) + let experimental_offer = ExperimentalOfferTlvStreamRef {}; + + (payer, offer, invoice_request, experimental_offer) } } @@ -793,12 +795,15 @@ impl Writeable for RefundContents { } } -type RefundTlvStream = (PayerTlvStream, OfferTlvStream, InvoiceRequestTlvStream); +type RefundTlvStream = ( + PayerTlvStream, OfferTlvStream, InvoiceRequestTlvStream, ExperimentalOfferTlvStream, +); type RefundTlvStreamRef<'a> = ( PayerTlvStreamRef<'a>, OfferTlvStreamRef<'a>, InvoiceRequestTlvStreamRef<'a>, + ExperimentalOfferTlvStreamRef, ); impl CursorReadable for RefundTlvStream { @@ -806,8 +811,9 @@ 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)?; - Ok((payer, offer, invoice_request)) + Ok((payer, offer, invoice_request, experimental_offer)) } } @@ -849,6 +855,7 @@ impl TryFrom for RefundContents { InvoiceRequestTlvStream { chain, amount, features, quantity, payer_id, payer_note, paths }, + _experimental_offer_tlv_stream, ) = tlv_stream; let payer = match payer_metadata { @@ -946,7 +953,7 @@ mod tests { use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT}; use crate::offers::invoice_request::{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 +1021,7 @@ mod tests { payer_note: None, paths: None, }, + ExperimentalOfferTlvStreamRef {}, ), ); @@ -1166,7 +1174,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 +1186,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 +1218,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 +1232,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 +1241,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 +1254,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 +1269,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 +1279,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 +1287,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 +1297,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 +1305,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"))); } diff --git a/lightning/src/offers/static_invoice.rs b/lightning/src/offers/static_invoice.rs index b82a369bad3..104588d9217 100644 --- a/lightning/src/offers/static_invoice.rs +++ b/lightning/src/offers/static_invoice.rs @@ -27,8 +27,8 @@ use crate::offers::merkle::{ }; use crate::offers::nonce::Nonce; use crate::offers::offer::{ - Amount, Offer, OfferContents, OfferTlvStream, OfferTlvStreamRef, Quantity, - EXPERIMENTAL_OFFER_TYPES, OFFER_TYPES, + Amount, ExperimentalOfferTlvStream, ExperimentalOfferTlvStreamRef, Offer, OfferContents, + OfferTlvStream, OfferTlvStreamRef, Quantity, EXPERIMENTAL_OFFER_TYPES, OFFER_TYPES, }; use crate::offers::parse::{Bolt12ParseError, Bolt12SemanticError, ParsedMessage}; use crate::util::ser::{CursorReadable, Iterable, WithoutLength, Writeable, Writer}; @@ -278,7 +278,7 @@ 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 (_, invoice_tlv_stream, _) = contents.as_tlv_stream(); // Allocate enough space for the invoice, which will include: // - all TLV records from `offer_bytes`, @@ -445,7 +445,9 @@ impl InvoiceContents { payment_hash: None, }; - (self.offer.as_tlv_stream(), invoice) + let (offer, experimental_offer) = self.offer.as_tlv_stream(); + + (offer, invoice, experimental_offer) } fn chain(&self) -> ChainHash { @@ -542,29 +544,41 @@ impl TryFrom> for StaticInvoice { } } -type FullInvoiceTlvStream = (OfferTlvStream, InvoiceTlvStream, SignatureTlvStream); +type FullInvoiceTlvStream = + (OfferTlvStream, InvoiceTlvStream, SignatureTlvStream, ExperimentalOfferTlvStream); 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)?; - Ok((offer, invoice, signature)) + Ok((offer, invoice, signature, experimental_offer)) } } -type PartialInvoiceTlvStream = (OfferTlvStream, InvoiceTlvStream); +type PartialInvoiceTlvStream = (OfferTlvStream, InvoiceTlvStream, ExperimentalOfferTlvStream); -type PartialInvoiceTlvStreamRef<'a> = (OfferTlvStreamRef<'a>, InvoiceTlvStreamRef<'a>); +type PartialInvoiceTlvStreamRef<'a> = + (OfferTlvStreamRef<'a>, InvoiceTlvStreamRef<'a>, ExperimentalOfferTlvStreamRef); 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, + ) = tlv_stream; + let contents = InvoiceContents::try_from(( + offer_tlv_stream, + invoice_tlv_stream, + experimental_offer_tlv_stream, + ))?; let signature = match signature { None => { @@ -600,6 +614,7 @@ impl TryFrom for InvoiceContents { payment_hash, amount, }, + experimental_offer_tlv_stream, ) = tlv_stream; if payment_hash.is_some() { @@ -632,7 +647,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, @@ -655,7 +670,9 @@ mod tests { use crate::offers::merkle; use crate::offers::merkle::{SignatureTlvStreamRef, TaggedHash}; 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, UnsignedStaticInvoice, DEFAULT_RELATIVE_EXPIRY, @@ -669,27 +686,39 @@ 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, + ); 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) = + self.contents.as_tlv_stream(); ( offer_tlv_stream, invoice_tlv_stream, SignatureTlvStreamRef { signature: Some(&self.signature) }, + experimental_offer_tlv_stream, ) } } fn tlv_stream_to_bytes( - tlv_stream: &(OfferTlvStreamRef, InvoiceTlvStreamRef, SignatureTlvStreamRef), + tlv_stream: &( + OfferTlvStreamRef, + InvoiceTlvStreamRef, + SignatureTlvStreamRef, + ExperimentalOfferTlvStreamRef, + ), ) -> 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(); buffer } @@ -819,6 +848,7 @@ mod tests { message_paths: Some(&paths), }, SignatureTlvStreamRef { signature: Some(&invoice.signature()) }, + ExperimentalOfferTlvStreamRef {}, ) ); @@ -933,7 +963,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(); @@ -969,7 +999,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(); diff --git a/lightning/src/util/ser.rs b/lightning/src/util/ser.rs index 99d20b927b6..3e528ce93e8 100644 --- a/lightning/src/util/ser.rs +++ b/lightning/src/util/ser.rs @@ -1446,6 +1446,26 @@ impl Writeable for (A, B } } +impl Readable for (A, B, C, D, E) { + 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)?; + let e: E = Readable::read(r)?; + Ok((a, b, c, d, e)) + } +} +impl Writeable for (A, B, C, D, E) { + 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)?; + self.4.write(w) + } +} + impl Writeable for () { fn write(&self, _: &mut W) -> Result<(), io::Error> { Ok(()) From 6a708e2d9fa6fbdb592e1e9e856430182e1927dd Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Tue, 6 Aug 2024 16:21:32 -0500 Subject: [PATCH 10/19] Test verification with experimental offer TLVs Offer metadata is generated from the offer TLVs and should included those in the experimental range. When verifying invoice request and invoice messages, these TLVs must be included. Similarly, OfferId construction should included these TLVs as well. Modify the BOLT12 verification tests to cover these TLVs. --- lightning/src/offers/invoice.rs | 11 ++++-- lightning/src/offers/invoice_request.rs | 6 +++- lightning/src/offers/offer.rs | 36 +++++++++++++++++-- lightning/src/offers/refund.rs | 30 ++++++++++++++-- lightning/src/offers/static_invoice.rs | 48 ++++++++++++++++++++++++- 5 files changed, 121 insertions(+), 10 deletions(-) diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index c8be0e3771e..fcc723474cf 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -1716,7 +1716,9 @@ mod tests { message_paths: None, }, SignatureTlvStreamRef { signature: Some(&invoice.signature()) }, - ExperimentalOfferTlvStreamRef {}, + ExperimentalOfferTlvStreamRef { + experimental_foo: None, + }, ), ); @@ -1810,7 +1812,9 @@ mod tests { message_paths: None, }, SignatureTlvStreamRef { signature: Some(&invoice.signature()) }, - ExperimentalOfferTlvStreamRef {}, + ExperimentalOfferTlvStreamRef { + experimental_foo: None, + }, ), ); @@ -1904,6 +1908,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() @@ -1925,6 +1930,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() @@ -1946,6 +1952,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 diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index 231c582d599..b80d82ce382 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -1434,7 +1434,9 @@ mod tests { paths: None, }, SignatureTlvStreamRef { signature: Some(&invoice_request.signature()) }, - ExperimentalOfferTlvStreamRef {}, + ExperimentalOfferTlvStreamRef { + experimental_foo: None, + }, ), ); @@ -1482,6 +1484,7 @@ 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) @@ -1565,6 +1568,7 @@ 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) diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs index 1c7eac6167e..3f7e049dc9c 100644 --- a/lightning/src/offers/offer.rs +++ b/lightning/src/offers/offer.rs @@ -225,6 +225,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, @@ -266,6 +268,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), @@ -464,6 +468,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() @@ -571,6 +581,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) => { @@ -1010,7 +1022,10 @@ impl OfferContents { issuer_id: self.issuer_signing_pubkey.as_ref(), }; - let experimental_offer = ExperimentalOfferTlvStreamRef {}; + let experimental_offer = ExperimentalOfferTlvStreamRef { + #[cfg(test)] + experimental_foo: self.experimental_foo, + }; (offer, experimental_offer) } @@ -1107,9 +1122,15 @@ tlv_stream!(OfferTlvStream, OfferTlvStreamRef<'a>, OFFER_TYPES, { /// 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); @@ -1157,7 +1178,10 @@ impl TryFrom for OfferContents { chains, metadata, currency, amount, description, features, absolute_expiry, paths, issuer, quantity_max, issuer_id, }, - ExperimentalOfferTlvStream {}, + ExperimentalOfferTlvStream { + #[cfg(test)] + experimental_foo, + }, ) = tlv_stream; let metadata = metadata.map(|metadata| Metadata::Bytes(metadata)); @@ -1196,6 +1220,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, }) } } @@ -1274,7 +1300,9 @@ mod tests { quantity_max: None, issuer_id: Some(&pubkey(42)), }, - ExperimentalOfferTlvStreamRef {}, + ExperimentalOfferTlvStreamRef { + experimental_foo: None, + }, ), ); @@ -1354,6 +1382,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)); @@ -1423,6 +1452,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)); diff --git a/lightning/src/offers/refund.rs b/lightning/src/offers/refund.rs index 8808c7c5002..a9f18d1b94d 100644 --- a/lightning/src/offers/refund.rs +++ b/lightning/src/offers/refund.rs @@ -176,6 +176,8 @@ 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, }, secp_ctx: None, }) @@ -218,6 +220,8 @@ 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, }, secp_ctx: Some(secp_ctx), }) @@ -358,6 +362,12 @@ 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 + } } } impl<'a> RefundBuilder<'a, secp256k1::SignOnly> { @@ -437,6 +447,8 @@ pub(super) struct RefundContents { payer_signing_pubkey: PublicKey, payer_note: Option, paths: Option>, + #[cfg(test)] + experimental_foo: Option, } impl Refund { @@ -770,7 +782,10 @@ impl RefundContents { paths: self.paths.as_ref(), }; - let experimental_offer = ExperimentalOfferTlvStreamRef {}; + let experimental_offer = ExperimentalOfferTlvStreamRef { + #[cfg(test)] + experimental_foo: self.experimental_foo, + }; (payer, offer, invoice_request, experimental_offer) } @@ -855,7 +870,10 @@ impl TryFrom for RefundContents { InvoiceRequestTlvStream { chain, amount, features, quantity, payer_id, payer_note, paths }, - _experimental_offer_tlv_stream, + ExperimentalOfferTlvStream { + #[cfg(test)] + experimental_foo, + }, ) = tlv_stream; let payer = match payer_metadata { @@ -916,6 +934,8 @@ 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, }) } } @@ -1021,7 +1041,9 @@ mod tests { payer_note: None, paths: None, }, - ExperimentalOfferTlvStreamRef {}, + ExperimentalOfferTlvStreamRef { + experimental_foo: None, + }, ), ); @@ -1050,6 +1072,7 @@ mod tests { let refund = RefundBuilder ::deriving_signing_pubkey(node_id, &expanded_key, nonce, &secp_ctx, 1000, payment_id) .unwrap() + .experimental_foo(42) .build().unwrap(); assert_eq!(refund.payer_signing_pubkey(), node_id); @@ -1117,6 +1140,7 @@ mod tests { ::deriving_signing_pubkey(node_id, &expanded_key, nonce, &secp_ctx, 1000, payment_id) .unwrap() .path(blinded_path) + .experimental_foo(42) .build().unwrap(); assert_ne!(refund.payer_signing_pubkey(), node_id); diff --git a/lightning/src/offers/static_invoice.rs b/lightning/src/offers/static_invoice.rs index 104588d9217..a2ea4032c64 100644 --- a/lightning/src/offers/static_invoice.rs +++ b/lightning/src/offers/static_invoice.rs @@ -848,7 +848,7 @@ mod tests { message_paths: Some(&paths), }, SignatureTlvStreamRef { signature: Some(&invoice.signature()) }, - ExperimentalOfferTlvStreamRef {}, + ExperimentalOfferTlvStreamRef { experimental_foo: None }, ) ); @@ -916,6 +916,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(); From a728d1dc2fca0c6dbd00a24a0b40874415d1ad1d Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Tue, 6 Aug 2024 11:22:19 -0500 Subject: [PATCH 11/19] Add parsing tests for experimental offer TLVs --- lightning/src/offers/offer.rs | 44 ++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs index 3f7e049dc9c..68f5c10fc21 100644 --- a/lightning/src/offers/offer.rs +++ b/lightning/src/offers/offer.rs @@ -1234,7 +1234,7 @@ impl core::fmt::Display for Offer { #[cfg(test)] mod tests { - use super::{Amount, ExperimentalOfferTlvStreamRef, OFFER_TYPES, Offer, OfferTlvStreamRef, Quantity}; + use super::{Amount, EXPERIMENTAL_OFFER_TYPES, ExperimentalOfferTlvStreamRef, OFFER_TYPES, Offer, OfferTlvStreamRef, Quantity}; #[cfg(not(c_bindings))] use { super::OfferBuilder, @@ -1962,6 +1962,35 @@ mod tests { } } + #[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(); @@ -1976,6 +2005,19 @@ mod tests { 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(EXPERIMENTAL_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)), + } } } From 62cddb7d3a5b69d427a147b18d4f2dd0fdd56ee0 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Wed, 7 Aug 2024 18:10:43 -0500 Subject: [PATCH 12/19] Add offer test vectors for experimental TLVs --- lightning/src/offers/offer.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs index 68f5c10fc21..8d999ff8084 100644 --- a/lightning/src/offers/offer.rs +++ b/lightning/src/offers/offer.rs @@ -2084,6 +2084,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::() { @@ -2226,6 +2229,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!( From 5590bc2fa8b28fc9c8f3999bd91e7655fccce5f7 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Thu, 8 Aug 2024 11:44:03 -0500 Subject: [PATCH 13/19] Parse experimental invreq TLV records The BOLT12 spec defines an experimental TLV range that are allowed in invoice_request messages. Allow this range when parsing an invoice request and include those bytes in any invoice. Also include those bytes when verifying that a Bolt12Invoice is for a valid InvoiceRequest. --- lightning/src/offers/invoice.rs | 59 ++++++++---- lightning/src/offers/invoice_request.rs | 122 +++++++++++++++--------- lightning/src/offers/refund.rs | 39 ++++---- lightning/src/util/ser.rs | 22 +++++ 4 files changed, 164 insertions(+), 78 deletions(-) diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index fcc723474cf..2c4e4e5ac12 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -121,7 +121,7 @@ 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::{EXPERIMENTAL_INVOICE_REQUEST_TYPES, INVOICE_REQUEST_PAYER_ID_TYPE, INVOICE_REQUEST_TYPES, IV_BYTES as INVOICE_REQUEST_IV_BYTES, InvoiceRequest, InvoiceRequestContents, InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef}; +use crate::offers::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, EXPERIMENTAL_OFFER_TYPES, ExperimentalOfferTlvStream, ExperimentalOfferTlvStreamRef, OFFER_TYPES, OfferTlvStream, OfferTlvStreamRef, Quantity}; @@ -497,7 +497,7 @@ impl UnsignedBolt12Invoice { const EXPERIMENTAL_TYPES: core::ops::Range = EXPERIMENTAL_OFFER_TYPES.start..EXPERIMENTAL_INVOICE_REQUEST_TYPES.end; - let (_, _, _, invoice_tlv_stream, _) = contents.as_tlv_stream(); + let (_, _, _, invoice_tlv_stream, _, _) = contents.as_tlv_stream(); // Allocate enough space for the invoice, which will include: // - all TLV records from `invreq_bytes` except signatures, @@ -903,7 +903,7 @@ impl Bolt12Invoice { pub(crate) fn as_tlv_stream(&self) -> FullInvoiceTlvStreamRef { let ( payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream, - experimental_offer_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), @@ -911,6 +911,7 @@ impl Bolt12Invoice { ( 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, ) } @@ -1172,13 +1173,15 @@ impl InvoiceContents { } fn as_tlv_stream(&self) -> PartialInvoiceTlvStreamRef { - let (payer, offer, invoice_request, experimental_offer) = 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(); - (payer, offer, invoice_request, invoice, experimental_offer) + (payer, offer, invoice_request, invoice, experimental_offer, experimental_invoice_request) } } @@ -1339,7 +1342,7 @@ impl_writeable!(FallbackAddress, { version, program }); type FullInvoiceTlvStream =( PayerTlvStream, OfferTlvStream, InvoiceRequestTlvStream, InvoiceTlvStream, SignatureTlvStream, - ExperimentalOfferTlvStream, + ExperimentalOfferTlvStream, ExperimentalInvoiceRequestTlvStream, ); type FullInvoiceTlvStreamRef<'a> = ( @@ -1349,6 +1352,7 @@ type FullInvoiceTlvStreamRef<'a> = ( InvoiceTlvStreamRef<'a>, SignatureTlvStreamRef<'a>, ExperimentalOfferTlvStreamRef, + ExperimentalInvoiceRequestTlvStreamRef, ); impl CursorReadable for FullInvoiceTlvStream { @@ -1359,14 +1363,20 @@ impl CursorReadable for FullInvoiceTlvStream { let invoice = 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, invoice, signature, experimental_offer)) + Ok( + ( + payer, offer, invoice_request, invoice, signature, experimental_offer, + experimental_invoice_request, + ) + ) } } type PartialInvoiceTlvStream = ( PayerTlvStream, OfferTlvStream, InvoiceRequestTlvStream, InvoiceTlvStream, - ExperimentalOfferTlvStream, + ExperimentalOfferTlvStream, ExperimentalInvoiceRequestTlvStream, ); type PartialInvoiceTlvStreamRef<'a> = ( @@ -1375,6 +1385,7 @@ type PartialInvoiceTlvStreamRef<'a> = ( InvoiceRequestTlvStreamRef<'a>, InvoiceTlvStreamRef<'a>, ExperimentalOfferTlvStreamRef, + ExperimentalInvoiceRequestTlvStreamRef, ); impl CursorReadable for PartialInvoiceTlvStream { @@ -1384,8 +1395,14 @@ impl CursorReadable for PartialInvoiceTlvStream { let invoice_request = CursorReadable::read(r)?; let invoice = CursorReadable::read(r)?; let experimental_offer = CursorReadable::read(r)?; + let experimental_invoice_request = CursorReadable::read(r)?; - Ok((payer, offer, invoice_request, invoice, experimental_offer)) + Ok( + ( + payer, offer, invoice_request, invoice, experimental_offer, + experimental_invoice_request, + ) + ) } } @@ -1398,11 +1415,12 @@ impl TryFrom> for Bolt12Invoice { payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream, SignatureTlvStream { signature }, experimental_offer_tlv_stream, + experimental_invoice_request_tlv_stream, ) = tlv_stream; let contents = InvoiceContents::try_from( ( payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream, - experimental_offer_tlv_stream, + experimental_offer_tlv_stream, experimental_invoice_request_tlv_stream, ) )?; @@ -1430,6 +1448,7 @@ impl TryFrom for InvoiceContents { features, node_id, message_paths, }, experimental_offer_tlv_stream, + experimental_invoice_request_tlv_stream, ) = tlv_stream; if message_paths.is_some() { return Err(Bolt12SemanticError::UnexpectedPaths) } @@ -1464,7 +1483,7 @@ impl TryFrom for InvoiceContents { let refund = RefundContents::try_from( ( payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, - experimental_offer_tlv_stream, + experimental_offer_tlv_stream, experimental_invoice_request_tlv_stream, ) )?; Ok(InvoiceContents::ForRefund { refund, fields }) @@ -1472,7 +1491,7 @@ impl TryFrom for InvoiceContents { let invoice_request = InvoiceRequestContents::try_from( ( payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, - experimental_offer_tlv_stream, + experimental_offer_tlv_stream, experimental_invoice_request_tlv_stream, ) )?; Ok(InvoiceContents::ForOffer { invoice_request, fields }) @@ -1545,7 +1564,7 @@ mod tests { use crate::ln::features::{Bolt12InvoiceFeatures, InvoiceRequestFeatures, OfferFeatures}; use crate::ln::inbound_payment::ExpandedKey; use crate::ln::msgs::DecodeError; - use crate::offers::invoice_request::InvoiceRequestTlvStreamRef; + use crate::offers::invoice_request::{ExperimentalInvoiceRequestTlvStreamRef, InvoiceRequestTlvStreamRef}; use crate::offers::merkle::{SignError, SignatureTlvStreamRef, TaggedHash, self}; use crate::offers::nonce::Nonce; use crate::offers::offer::{Amount, ExperimentalOfferTlvStreamRef, OfferTlvStreamRef, Quantity}; @@ -1719,6 +1738,7 @@ mod tests { ExperimentalOfferTlvStreamRef { experimental_foo: None, }, + ExperimentalInvoiceRequestTlvStreamRef {}, ), ); @@ -1815,6 +1835,7 @@ mod tests { ExperimentalOfferTlvStreamRef { experimental_foo: None, }, + ExperimentalInvoiceRequestTlvStreamRef {}, ), ); @@ -2011,7 +2032,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); @@ -2027,7 +2048,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)); @@ -2046,7 +2067,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)); } @@ -2064,7 +2085,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)); @@ -2102,7 +2123,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![ @@ -2145,7 +2166,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)); } diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index b80d82ce382..dd0d523588c 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -523,7 +523,7 @@ impl UnsignedInvoiceRequest { // unknown TLV records, which are not stored in `OfferContents`. let ( payer_tlv_stream, _offer_tlv_stream, invoice_request_tlv_stream, - _experimental_offer_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: @@ -535,6 +535,7 @@ impl UnsignedInvoiceRequest { + payer_tlv_stream.serialized_length() + invoice_request_tlv_stream.serialized_length() + SIGNATURE_TLV_RECORD_SIZE + + experimental_invoice_request_tlv_stream.serialized_length(), ); payer_tlv_stream.write(&mut bytes).unwrap(); @@ -555,12 +556,14 @@ impl UnsignedInvoiceRequest { - 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)); @@ -910,7 +913,7 @@ impl InvoiceRequest { pub(crate) fn as_tlv_stream(&self) -> FullInvoiceRequestTlvStreamRef { let ( payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, - experimental_offer_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), @@ -918,6 +921,7 @@ impl InvoiceRequest { ( payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, signature_tlv_stream, experimental_offer_tlv_stream, + experimental_invoice_request_tlv_stream, ) } } @@ -1034,9 +1038,10 @@ impl InvoiceRequestContents { } pub(super) fn as_tlv_stream(&self) -> PartialInvoiceRequestTlvStreamRef { - let (payer, offer, mut invoice_request, experimental_offer) = 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, experimental_offer) + (payer, offer, invoice_request, experimental_offer, experimental_invoice_request) } } @@ -1071,7 +1076,9 @@ impl InvoiceRequestContentsWithoutPayerSigningPubkey { paths: None, }; - (payer, offer, invoice_request, experimental_offer) + let experimental_invoice_request = ExperimentalInvoiceRequestTlvStreamRef {}; + + (payer, offer, invoice_request, experimental_offer, experimental_invoice_request) } } @@ -1126,9 +1133,14 @@ tlv_stream!(InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef<'a>, INVOICE_REQ pub(super) const EXPERIMENTAL_INVOICE_REQUEST_TYPES: core::ops::Range = 2_000_000_000..3_000_000_000; +tlv_stream!( + ExperimentalInvoiceRequestTlvStream, ExperimentalInvoiceRequestTlvStreamRef, + EXPERIMENTAL_INVOICE_REQUEST_TYPES, {} +); + type FullInvoiceRequestTlvStream = ( PayerTlvStream, OfferTlvStream, InvoiceRequestTlvStream, SignatureTlvStream, - ExperimentalOfferTlvStream, + ExperimentalOfferTlvStream, ExperimentalInvoiceRequestTlvStream, ); type FullInvoiceRequestTlvStreamRef<'a> = ( @@ -1137,6 +1149,7 @@ type FullInvoiceRequestTlvStreamRef<'a> = ( InvoiceRequestTlvStreamRef<'a>, SignatureTlvStreamRef<'a>, ExperimentalOfferTlvStreamRef, + ExperimentalInvoiceRequestTlvStreamRef, ); impl CursorReadable for FullInvoiceRequestTlvStream { @@ -1146,13 +1159,20 @@ impl CursorReadable for FullInvoiceRequestTlvStream { 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, experimental_offer)) + Ok( + ( + payer, offer, invoice_request, signature, experimental_offer, + experimental_invoice_request, + ) + ) } } type PartialInvoiceRequestTlvStream = ( PayerTlvStream, OfferTlvStream, InvoiceRequestTlvStream, ExperimentalOfferTlvStream, + ExperimentalInvoiceRequestTlvStream, ); type PartialInvoiceRequestTlvStreamRef<'a> = ( @@ -1160,6 +1180,7 @@ type PartialInvoiceRequestTlvStreamRef<'a> = ( OfferTlvStreamRef<'a>, InvoiceRequestTlvStreamRef<'a>, ExperimentalOfferTlvStreamRef, + ExperimentalInvoiceRequestTlvStreamRef, ); impl TryFrom> for UnsignedInvoiceRequest { @@ -1192,11 +1213,12 @@ impl TryFrom> for InvoiceRequest { 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, - experimental_offer_tlv_stream, + experimental_offer_tlv_stream, experimental_invoice_request_tlv_stream, ) )?; @@ -1222,6 +1244,7 @@ impl TryFrom for InvoiceRequestContents { chain, amount, features, quantity, payer_id, payer_note, paths, }, experimental_offer_tlv_stream, + ExperimentalInvoiceRequestTlvStream {}, ) = tlv_stream; let payer = match metadata { @@ -1309,7 +1332,7 @@ impl Readable for InvoiceRequestFields { #[cfg(test)] mod tests { - use super::{INVOICE_REQUEST_TYPES, InvoiceRequest, InvoiceRequestFields, InvoiceRequestTlvStreamRef, PAYER_NOTE_LIMIT, SIGNATURE_TAG, UnsignedInvoiceRequest}; + use super::{ExperimentalInvoiceRequestTlvStreamRef, INVOICE_REQUEST_TYPES, InvoiceRequest, InvoiceRequestFields, InvoiceRequestTlvStreamRef, PAYER_NOTE_LIMIT, SIGNATURE_TAG, UnsignedInvoiceRequest}; use bitcoin::constants::ChainHash; use bitcoin::network::Network; @@ -1437,6 +1460,7 @@ mod tests { ExperimentalOfferTlvStreamRef { experimental_foo: None, }, + ExperimentalInvoiceRequestTlvStreamRef {}, ), ); @@ -1509,24 +1533,27 @@ mod tests { let ( payer_tlv_stream, offer_tlv_stream, mut invoice_request_tlv_stream, mut invoice_tlv_stream, mut signature_tlv_stream, experimental_offer_tlv_stream, + experimental_invoice_request_tlv_stream, ) = 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, + ); let mut bytes = Vec::new(); - tlv_stream.write(&mut bytes).unwrap(); - experimental_offer_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 = Vec::new(); - tlv_stream.write(&mut encoded_invoice).unwrap(); - signature_tlv_stream.write(&mut encoded_invoice).unwrap(); - experimental_offer_tlv_stream.write(&mut encoded_invoice).unwrap(); + (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()); @@ -1535,24 +1562,27 @@ mod tests { let ( mut payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream, mut signature_tlv_stream, experimental_offer_tlv_stream, + experimental_invoice_request_tlv_stream, ) = 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, + ); let mut bytes = Vec::new(); - tlv_stream.write(&mut bytes).unwrap(); - experimental_offer_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 = Vec::new(); - tlv_stream.write(&mut encoded_invoice).unwrap(); - signature_tlv_stream.write(&mut encoded_invoice).unwrap(); - experimental_offer_tlv_stream.write(&mut encoded_invoice).unwrap(); + (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()); @@ -1589,24 +1619,27 @@ mod tests { let ( payer_tlv_stream, offer_tlv_stream, mut invoice_request_tlv_stream, mut invoice_tlv_stream, mut signature_tlv_stream, experimental_offer_tlv_stream, + experimental_invoice_request_tlv_stream, ) = 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, + ); let mut bytes = Vec::new(); - tlv_stream.write(&mut bytes).unwrap(); - experimental_offer_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 = Vec::new(); - tlv_stream.write(&mut encoded_invoice).unwrap(); - signature_tlv_stream.write(&mut encoded_invoice).unwrap(); - experimental_offer_tlv_stream.write(&mut encoded_invoice).unwrap(); + (tlv_stream, signature_tlv_stream, experimental_tlv_stream) + .write(&mut encoded_invoice) + .unwrap(); let invoice = Bolt12Invoice::try_from(encoded_invoice).unwrap(); assert!( @@ -1617,24 +1650,27 @@ mod tests { let ( payer_tlv_stream, offer_tlv_stream, mut invoice_request_tlv_stream, invoice_tlv_stream, mut signature_tlv_stream, experimental_offer_tlv_stream, + experimental_invoice_request_tlv_stream, ) = 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, + ); let mut bytes = Vec::new(); - tlv_stream.write(&mut bytes).unwrap(); - experimental_offer_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 = Vec::new(); - tlv_stream.write(&mut encoded_invoice).unwrap(); - signature_tlv_stream.write(&mut encoded_invoice).unwrap(); - experimental_offer_tlv_stream.write(&mut encoded_invoice).unwrap(); + (tlv_stream, signature_tlv_stream, experimental_tlv_stream) + .write(&mut encoded_invoice) + .unwrap(); let invoice = Bolt12Invoice::try_from(encoded_invoice).unwrap(); assert!( @@ -1654,7 +1690,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); @@ -1666,7 +1702,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)); @@ -1679,7 +1715,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); @@ -1693,7 +1729,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)); @@ -1729,7 +1765,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)); @@ -1741,7 +1777,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)); @@ -1752,7 +1788,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)); @@ -1832,7 +1868,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())); @@ -1844,7 +1880,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); } @@ -1861,7 +1897,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); @@ -1886,7 +1922,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)); @@ -1911,7 +1947,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)); @@ -1947,7 +1983,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"))); @@ -1959,7 +1995,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"))); } diff --git a/lightning/src/offers/refund.rs b/lightning/src/offers/refund.rs index a9f18d1b94d..2eb54b12e10 100644 --- a/lightning/src/offers/refund.rs +++ b/lightning/src/offers/refund.rs @@ -98,7 +98,7 @@ use crate::ln::channelmanager::PaymentId; use crate::ln::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::{ExperimentalOfferTlvStream, ExperimentalOfferTlvStreamRef, OfferTlvStream, OfferTlvStreamRef}; use crate::offers::parse::{Bech32Encode, Bolt12ParseError, Bolt12SemanticError, ParsedMessage}; @@ -787,7 +787,9 @@ impl RefundContents { experimental_foo: self.experimental_foo, }; - (payer, offer, invoice_request, experimental_offer) + let experimental_invoice_request = ExperimentalInvoiceRequestTlvStreamRef {}; + + (payer, offer, invoice_request, experimental_offer, experimental_invoice_request) } } @@ -812,6 +814,7 @@ impl Writeable for RefundContents { type RefundTlvStream = ( PayerTlvStream, OfferTlvStream, InvoiceRequestTlvStream, ExperimentalOfferTlvStream, + ExperimentalInvoiceRequestTlvStream, ); type RefundTlvStreamRef<'a> = ( @@ -819,6 +822,7 @@ type RefundTlvStreamRef<'a> = ( OfferTlvStreamRef<'a>, InvoiceRequestTlvStreamRef<'a>, ExperimentalOfferTlvStreamRef, + ExperimentalInvoiceRequestTlvStreamRef, ); impl CursorReadable for RefundTlvStream { @@ -827,8 +831,9 @@ impl CursorReadable for RefundTlvStream { 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, experimental_offer)) + Ok((payer, offer, invoice_request, experimental_offer, experimental_invoice_request)) } } @@ -874,6 +879,7 @@ impl TryFrom for RefundContents { #[cfg(test)] experimental_foo, }, + ExperimentalInvoiceRequestTlvStream {}, ) = tlv_stream; let payer = match payer_metadata { @@ -971,7 +977,7 @@ mod tests { use crate::ln::features::{InvoiceRequestFeatures, OfferFeatures}; use crate::ln::inbound_payment::ExpandedKey; use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT}; - use crate::offers::invoice_request::{INVOICE_REQUEST_TYPES, InvoiceRequestTlvStreamRef}; + use crate::offers::invoice_request::{ExperimentalInvoiceRequestTlvStreamRef, INVOICE_REQUEST_TYPES, InvoiceRequestTlvStreamRef}; use crate::offers::nonce::Nonce; use crate::offers::offer::{ExperimentalOfferTlvStreamRef, OfferTlvStreamRef}; use crate::offers::parse::{Bolt12ParseError, Bolt12SemanticError}; @@ -1044,6 +1050,7 @@ mod tests { ExperimentalOfferTlvStreamRef { experimental_foo: None, }, + ExperimentalInvoiceRequestTlvStreamRef {}, ), ); @@ -1198,7 +1205,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)); @@ -1210,7 +1217,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)); @@ -1242,7 +1249,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)); @@ -1256,7 +1263,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"))); @@ -1265,7 +1272,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"))); } @@ -1278,14 +1285,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)); @@ -1293,7 +1300,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)); } @@ -1303,7 +1310,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)); @@ -1311,7 +1318,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)); } @@ -1321,7 +1328,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"))); @@ -1329,7 +1336,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"))); } diff --git a/lightning/src/util/ser.rs b/lightning/src/util/ser.rs index 3e528ce93e8..a3b56fa8494 100644 --- a/lightning/src/util/ser.rs +++ b/lightning/src/util/ser.rs @@ -1466,6 +1466,28 @@ impl Write } } +impl Readable for (A, B, C, D, E, F) { + 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)?; + let e: E = Readable::read(r)?; + let f: F = Readable::read(r)?; + Ok((a, b, c, d, e, f)) + } +} +impl Writeable for (A, B, C, D, E, F) { + 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)?; + self.4.write(w)?; + self.5.write(w) + } +} + impl Writeable for () { fn write(&self, _: &mut W) -> Result<(), io::Error> { Ok(()) From e217c628635669bb2800f1e79bfb9ec3eea37f51 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Thu, 8 Aug 2024 16:50:26 -0500 Subject: [PATCH 14/19] Test verification with experimental invreq TLVs Payer metadata is generated from the invreq TLVs and should included those in the experimental range. When verifying invoice messages, these TLVs must be included. Modify the BOLT12 verification tests to cover them. --- lightning/src/offers/invoice.rs | 8 +++-- lightning/src/offers/invoice_request.rs | 41 ++++++++++++++++++++++--- lightning/src/offers/refund.rs | 30 ++++++++++++++++-- 3 files changed, 70 insertions(+), 9 deletions(-) diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index 2c4e4e5ac12..eb2a0cdccea 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -1738,7 +1738,9 @@ mod tests { ExperimentalOfferTlvStreamRef { experimental_foo: None, }, - ExperimentalInvoiceRequestTlvStreamRef {}, + ExperimentalInvoiceRequestTlvStreamRef { + experimental_bar: None, + }, ), ); @@ -1835,7 +1837,9 @@ mod tests { ExperimentalOfferTlvStreamRef { experimental_foo: None, }, - ExperimentalInvoiceRequestTlvStreamRef {}, + ExperimentalInvoiceRequestTlvStreamRef { + experimental_bar: None, + }, ), ); diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index dd0d523588c..2a618bae5f8 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -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 @@ -691,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) => { @@ -994,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; @@ -1076,7 +1088,10 @@ impl InvoiceRequestContentsWithoutPayerSigningPubkey { paths: None, }; - let experimental_invoice_request = ExperimentalInvoiceRequestTlvStreamRef {}; + let experimental_invoice_request = ExperimentalInvoiceRequestTlvStreamRef { + #[cfg(test)] + experimental_bar: self.experimental_bar, + }; (payer, offer, invoice_request, experimental_offer, experimental_invoice_request) } @@ -1133,11 +1148,20 @@ tlv_stream!(InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef<'a>, INVOICE_REQ 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, @@ -1244,7 +1268,10 @@ impl TryFrom for InvoiceRequestContents { chain, amount, features, quantity, payer_id, payer_note, paths, }, experimental_offer_tlv_stream, - ExperimentalInvoiceRequestTlvStream {}, + ExperimentalInvoiceRequestTlvStream { + #[cfg(test)] + experimental_bar, + }, ) = tlv_stream; let payer = match metadata { @@ -1278,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, }) @@ -1460,7 +1489,9 @@ mod tests { ExperimentalOfferTlvStreamRef { experimental_foo: None, }, - ExperimentalInvoiceRequestTlvStreamRef {}, + ExperimentalInvoiceRequestTlvStreamRef { + experimental_bar: None, + }, ), ); @@ -1513,6 +1544,7 @@ mod tests { 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()); @@ -1603,6 +1635,7 @@ mod tests { let invoice_request = offer .request_invoice_deriving_signing_pubkey(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() + .experimental_bar(42) .build_and_sign() .unwrap(); diff --git a/lightning/src/offers/refund.rs b/lightning/src/offers/refund.rs index 2eb54b12e10..1d29785c533 100644 --- a/lightning/src/offers/refund.rs +++ b/lightning/src/offers/refund.rs @@ -178,6 +178,8 @@ macro_rules! refund_explicit_metadata_builder_methods { () => { 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, }) @@ -222,6 +224,8 @@ macro_rules! refund_builder_methods { ( 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), }) @@ -368,6 +372,12 @@ macro_rules! refund_builder_test_methods { ( $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> { @@ -449,6 +459,8 @@ pub(super) struct RefundContents { paths: Option>, #[cfg(test)] experimental_foo: Option, + #[cfg(test)] + experimental_bar: Option, } impl Refund { @@ -787,7 +799,10 @@ impl RefundContents { experimental_foo: self.experimental_foo, }; - let experimental_invoice_request = ExperimentalInvoiceRequestTlvStreamRef {}; + let experimental_invoice_request = ExperimentalInvoiceRequestTlvStreamRef { + #[cfg(test)] + experimental_bar: self.experimental_bar, + }; (payer, offer, invoice_request, experimental_offer, experimental_invoice_request) } @@ -879,7 +894,10 @@ impl TryFrom for RefundContents { #[cfg(test)] experimental_foo, }, - ExperimentalInvoiceRequestTlvStream {}, + ExperimentalInvoiceRequestTlvStream { + #[cfg(test)] + experimental_bar, + }, ) = tlv_stream; let payer = match payer_metadata { @@ -942,6 +960,8 @@ impl TryFrom for RefundContents { payer_signing_pubkey, payer_note, paths, #[cfg(test)] experimental_foo, + #[cfg(test)] + experimental_bar, }) } } @@ -1050,7 +1070,9 @@ mod tests { ExperimentalOfferTlvStreamRef { experimental_foo: None, }, - ExperimentalInvoiceRequestTlvStreamRef {}, + ExperimentalInvoiceRequestTlvStreamRef { + experimental_bar: None, + }, ), ); @@ -1080,6 +1102,7 @@ mod tests { ::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); @@ -1148,6 +1171,7 @@ mod tests { .unwrap() .path(blinded_path) .experimental_foo(42) + .experimental_bar(42) .build().unwrap(); assert_ne!(refund.payer_signing_pubkey(), node_id); From 9cf178f28f18683a5a0a2d125c3bc8f05bd922bb Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Fri, 9 Aug 2024 18:05:20 -0500 Subject: [PATCH 15/19] Add parsing tests for experimental invreq TLVs --- lightning/src/offers/invoice_request.rs | 123 +++++++++++++++++++++++- 1 file changed, 121 insertions(+), 2 deletions(-) diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index 2a618bae5f8..ba66297f33c 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -1361,7 +1361,7 @@ impl Readable for InvoiceRequestFields { #[cfg(test)] mod tests { - use super::{ExperimentalInvoiceRequestTlvStreamRef, INVOICE_REQUEST_TYPES, 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; @@ -1375,7 +1375,7 @@ 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, ExperimentalOfferTlvStreamRef, OfferTlvStreamRef, Quantity}; #[cfg(not(c_bindings))] @@ -2525,10 +2525,118 @@ mod tests { } } + #[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() @@ -2549,6 +2657,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] From 22d30e6b8385b181bf5e9f053df4944d0a7c97a1 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Thu, 24 Oct 2024 15:35:25 -0500 Subject: [PATCH 16/19] Add parsing tests for experimental refund TLVs --- lightning/src/offers/refund.rs | 50 +++++++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/lightning/src/offers/refund.rs b/lightning/src/offers/refund.rs index 1d29785c533..fd0f0916f76 100644 --- a/lightning/src/offers/refund.rs +++ b/lightning/src/offers/refund.rs @@ -997,7 +997,7 @@ mod tests { use crate::ln::features::{InvoiceRequestFeatures, OfferFeatures}; use crate::ln::inbound_payment::ExpandedKey; use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT}; - use crate::offers::invoice_request::{ExperimentalInvoiceRequestTlvStreamRef, INVOICE_REQUEST_TYPES, InvoiceRequestTlvStreamRef}; + use crate::offers::invoice_request::{EXPERIMENTAL_INVOICE_REQUEST_TYPES, ExperimentalInvoiceRequestTlvStreamRef, INVOICE_REQUEST_TYPES, InvoiceRequestTlvStreamRef}; use crate::offers::nonce::Nonce; use crate::offers::offer::{ExperimentalOfferTlvStreamRef, OfferTlvStreamRef}; use crate::offers::parse::{Bolt12ParseError, Bolt12SemanticError}; @@ -1621,6 +1621,43 @@ mod tests { } } + #[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(); @@ -1638,5 +1675,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)), + } } } From 8e279f719dcc1677714a26c56140193bd70cfe58 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Mon, 12 Aug 2024 16:54:55 -0500 Subject: [PATCH 17/19] Parse experimental invoice TLV records The BOLT12 spec defines an experimental TLV range that is allowed in offer and invoice_request messages. The remaining TLV-space is for experimental use in invoice messages. Allow this range when parsing an invoice and include it when signing one. --- lightning/src/offers/invoice.rs | 83 ++++++++++++++++--------- lightning/src/offers/invoice_request.rs | 12 ++-- lightning/src/offers/static_invoice.rs | 61 +++++++++++++----- lightning/src/util/ser.rs | 24 +++++++ 4 files changed, 134 insertions(+), 46 deletions(-) diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index eb2a0cdccea..08c4c6a78b7 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -497,7 +497,8 @@ impl UnsignedBolt12Invoice { const EXPERIMENTAL_TYPES: core::ops::Range = EXPERIMENTAL_OFFER_TYPES.start..EXPERIMENTAL_INVOICE_REQUEST_TYPES.end; - let (_, _, _, invoice_tlv_stream, _, _) = contents.as_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 `invreq_bytes` except signatures, @@ -510,6 +511,7 @@ impl UnsignedBolt12Invoice { 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 @@ -531,12 +533,14 @@ impl UnsignedBolt12Invoice { - 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)); @@ -904,6 +908,7 @@ impl Bolt12Invoice { let ( payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream, experimental_offer_tlv_stream, experimental_invoice_request_tlv_stream, + experimental_invoice_tlv_stream, ) = self.contents.as_tlv_stream(); let signature_tlv_stream = SignatureTlvStreamRef { signature: Some(&self.signature), @@ -911,7 +916,7 @@ impl Bolt12Invoice { ( payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream, signature_tlv_stream, experimental_offer_tlv_stream, - experimental_invoice_request_tlv_stream, + experimental_invoice_request_tlv_stream, experimental_invoice_tlv_stream, ) } @@ -1179,9 +1184,12 @@ impl InvoiceContents { InvoiceContents::ForOffer { invoice_request, .. } => invoice_request.as_tlv_stream(), InvoiceContents::ForRefund { refund, .. } => refund.as_tlv_stream(), }; - let invoice = self.fields().as_tlv_stream(); + let (invoice, experimental_invoice) = self.fields().as_tlv_stream(); - (payer, offer, invoice_request, invoice, experimental_offer, experimental_invoice_request) + ( + payer, offer, invoice_request, invoice, experimental_offer, + experimental_invoice_request, experimental_invoice, + ) } } @@ -1229,24 +1237,27 @@ pub(super) fn filter_fallbacks( } impl InvoiceFields { - fn as_tlv_stream(&self) -> InvoiceTlvStreamRef { + fn as_tlv_stream(&self) -> (InvoiceTlvStreamRef, ExperimentalInvoiceTlvStreamRef) { let features = { if self.features == Bolt12InvoiceFeatures::empty() { None } else { Some(&self.features) } }; - InvoiceTlvStreamRef { - paths: Some(Iterable(self.payment_paths.iter().map(|path| path.inner_blinded_path()))), - blindedpay: Some(Iterable(self.payment_paths.iter().map(|path| &path.payinfo))), - created_at: Some(self.created_at.as_secs()), - relative_expiry: self.relative_expiry.map(|duration| duration.as_secs() as u32), - payment_hash: Some(&self.payment_hash), - amount: Some(self.amount_msats), - fallbacks: self.fallbacks.as_ref(), - features, - node_id: Some(&self.signing_pubkey), - message_paths: None, - } + ( + InvoiceTlvStreamRef { + paths: Some(Iterable(self.payment_paths.iter().map(|path| path.inner_blinded_path()))), + blindedpay: Some(Iterable(self.payment_paths.iter().map(|path| &path.payinfo))), + created_at: Some(self.created_at.as_secs()), + relative_expiry: self.relative_expiry.map(|duration| duration.as_secs() as u32), + payment_hash: Some(&self.payment_hash), + amount: Some(self.amount_msats), + fallbacks: self.fallbacks.as_ref(), + features, + node_id: Some(&self.signing_pubkey), + message_paths: None, + }, + ExperimentalInvoiceTlvStreamRef {}, + ) } } @@ -1321,6 +1332,13 @@ tlv_stream!(InvoiceTlvStream, InvoiceTlvStreamRef<'a>, INVOICE_TYPES, { (236, message_paths: (Vec, WithoutLength)), }); +/// Valid type range for experimental invoice TLV records. +const EXPERIMENTAL_INVOICE_TYPES: core::ops::RangeFrom = 3_000_000_000..; + +tlv_stream!( + ExperimentalInvoiceTlvStream, ExperimentalInvoiceTlvStreamRef, EXPERIMENTAL_INVOICE_TYPES, {} +); + pub(super) type BlindedPathIter<'a> = core::iter::Map< core::slice::Iter<'a, BlindedPaymentPath>, for<'r> fn(&'r BlindedPaymentPath) -> &'r BlindedPath, @@ -1342,7 +1360,7 @@ impl_writeable!(FallbackAddress, { version, program }); type FullInvoiceTlvStream =( PayerTlvStream, OfferTlvStream, InvoiceRequestTlvStream, InvoiceTlvStream, SignatureTlvStream, - ExperimentalOfferTlvStream, ExperimentalInvoiceRequestTlvStream, + ExperimentalOfferTlvStream, ExperimentalInvoiceRequestTlvStream, ExperimentalInvoiceTlvStream, ); type FullInvoiceTlvStreamRef<'a> = ( @@ -1353,6 +1371,7 @@ type FullInvoiceTlvStreamRef<'a> = ( SignatureTlvStreamRef<'a>, ExperimentalOfferTlvStreamRef, ExperimentalInvoiceRequestTlvStreamRef, + ExperimentalInvoiceTlvStreamRef, ); impl CursorReadable for FullInvoiceTlvStream { @@ -1364,11 +1383,12 @@ impl CursorReadable for FullInvoiceTlvStream { let signature = CursorReadable::read(r)?; let experimental_offer = CursorReadable::read(r)?; let experimental_invoice_request = CursorReadable::read(r)?; + let experimental_invoice = CursorReadable::read(r)?; Ok( ( payer, offer, invoice_request, invoice, signature, experimental_offer, - experimental_invoice_request, + experimental_invoice_request, experimental_invoice, ) ) } @@ -1376,7 +1396,7 @@ impl CursorReadable for FullInvoiceTlvStream { type PartialInvoiceTlvStream = ( PayerTlvStream, OfferTlvStream, InvoiceRequestTlvStream, InvoiceTlvStream, - ExperimentalOfferTlvStream, ExperimentalInvoiceRequestTlvStream, + ExperimentalOfferTlvStream, ExperimentalInvoiceRequestTlvStream, ExperimentalInvoiceTlvStream, ); type PartialInvoiceTlvStreamRef<'a> = ( @@ -1386,6 +1406,7 @@ type PartialInvoiceTlvStreamRef<'a> = ( InvoiceTlvStreamRef<'a>, ExperimentalOfferTlvStreamRef, ExperimentalInvoiceRequestTlvStreamRef, + ExperimentalInvoiceTlvStreamRef, ); impl CursorReadable for PartialInvoiceTlvStream { @@ -1396,11 +1417,12 @@ impl CursorReadable for PartialInvoiceTlvStream { let invoice = CursorReadable::read(r)?; let experimental_offer = CursorReadable::read(r)?; let experimental_invoice_request = CursorReadable::read(r)?; + let experimental_invoice = CursorReadable::read(r)?; Ok( ( payer, offer, invoice_request, invoice, experimental_offer, - experimental_invoice_request, + experimental_invoice_request, experimental_invoice, ) ) } @@ -1416,11 +1438,13 @@ impl TryFrom> for Bolt12Invoice { SignatureTlvStream { signature }, experimental_offer_tlv_stream, experimental_invoice_request_tlv_stream, + experimental_invoice_tlv_stream, ) = tlv_stream; let contents = InvoiceContents::try_from( ( payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream, experimental_offer_tlv_stream, experimental_invoice_request_tlv_stream, + experimental_invoice_tlv_stream, ) )?; @@ -1449,6 +1473,7 @@ impl TryFrom for InvoiceContents { }, experimental_offer_tlv_stream, experimental_invoice_request_tlv_stream, + ExperimentalInvoiceTlvStream {}, ) = tlv_stream; if message_paths.is_some() { return Err(Bolt12SemanticError::UnexpectedPaths) } @@ -1545,7 +1570,7 @@ pub(super) fn check_invoice_signing_pubkey( #[cfg(test)] mod tests { - use super::{Bolt12Invoice, DEFAULT_RELATIVE_EXPIRY, FallbackAddress, FullInvoiceTlvStreamRef, INVOICE_TYPES, InvoiceTlvStreamRef, SIGNATURE_TAG, UnsignedBolt12Invoice}; + use super::{Bolt12Invoice, DEFAULT_RELATIVE_EXPIRY, ExperimentalInvoiceTlvStreamRef, FallbackAddress, FullInvoiceTlvStreamRef, INVOICE_TYPES, InvoiceTlvStreamRef, SIGNATURE_TAG, UnsignedBolt12Invoice}; use bitcoin::{CompressedPublicKey, WitnessProgram, WitnessVersion}; use bitcoin::constants::ChainHash; @@ -1741,6 +1766,7 @@ mod tests { ExperimentalInvoiceRequestTlvStreamRef { experimental_bar: None, }, + ExperimentalInvoiceTlvStreamRef {}, ), ); @@ -1840,6 +1866,7 @@ mod tests { ExperimentalInvoiceRequestTlvStreamRef { experimental_bar: None, }, + ExperimentalInvoiceTlvStreamRef {}, ), ); @@ -2036,7 +2063,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); @@ -2052,7 +2079,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)); @@ -2071,7 +2098,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)); } @@ -2089,7 +2116,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)); @@ -2127,7 +2154,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![ @@ -2170,7 +2197,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)); } diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index ba66297f33c..55fe9af1102 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -1565,7 +1565,7 @@ mod tests { let ( payer_tlv_stream, offer_tlv_stream, mut invoice_request_tlv_stream, mut invoice_tlv_stream, mut signature_tlv_stream, experimental_offer_tlv_stream, - experimental_invoice_request_tlv_stream, + experimental_invoice_request_tlv_stream, experimental_invoice_tlv_stream, ) = invoice.as_tlv_stream(); invoice_request_tlv_stream.amount = Some(2000); invoice_tlv_stream.amount = Some(2000); @@ -1574,6 +1574,7 @@ mod tests { (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream); let experimental_tlv_stream = ( experimental_offer_tlv_stream, experimental_invoice_request_tlv_stream, + experimental_invoice_tlv_stream, ); let mut bytes = Vec::new(); (&tlv_stream, &experimental_tlv_stream).write(&mut bytes).unwrap(); @@ -1594,7 +1595,7 @@ mod tests { let ( mut payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream, mut signature_tlv_stream, experimental_offer_tlv_stream, - experimental_invoice_request_tlv_stream, + experimental_invoice_request_tlv_stream, experimental_invoice_tlv_stream, ) = invoice.as_tlv_stream(); let metadata = payer_tlv_stream.metadata.unwrap().iter().copied().rev().collect(); payer_tlv_stream.metadata = Some(&metadata); @@ -1603,6 +1604,7 @@ mod tests { (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream); let experimental_tlv_stream = ( experimental_offer_tlv_stream, experimental_invoice_request_tlv_stream, + experimental_invoice_tlv_stream, ); let mut bytes = Vec::new(); (&tlv_stream, &experimental_tlv_stream).write(&mut bytes).unwrap(); @@ -1652,7 +1654,7 @@ mod tests { let ( payer_tlv_stream, offer_tlv_stream, mut invoice_request_tlv_stream, mut invoice_tlv_stream, mut signature_tlv_stream, experimental_offer_tlv_stream, - experimental_invoice_request_tlv_stream, + experimental_invoice_request_tlv_stream, experimental_invoice_tlv_stream, ) = invoice.as_tlv_stream(); invoice_request_tlv_stream.amount = Some(2000); invoice_tlv_stream.amount = Some(2000); @@ -1661,6 +1663,7 @@ mod tests { (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream); let experimental_tlv_stream = ( experimental_offer_tlv_stream, experimental_invoice_request_tlv_stream, + experimental_invoice_tlv_stream, ); let mut bytes = Vec::new(); (&tlv_stream, &experimental_tlv_stream).write(&mut bytes).unwrap(); @@ -1683,7 +1686,7 @@ mod tests { let ( payer_tlv_stream, offer_tlv_stream, mut invoice_request_tlv_stream, invoice_tlv_stream, mut signature_tlv_stream, experimental_offer_tlv_stream, - experimental_invoice_request_tlv_stream, + experimental_invoice_request_tlv_stream, experimental_invoice_tlv_stream, ) = invoice.as_tlv_stream(); let payer_id = pubkey(1); invoice_request_tlv_stream.payer_id = Some(&payer_id); @@ -1692,6 +1695,7 @@ mod tests { (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream); let experimental_tlv_stream = ( experimental_offer_tlv_stream, experimental_invoice_request_tlv_stream, + experimental_invoice_tlv_stream, ); let mut bytes = Vec::new(); (&tlv_stream, &experimental_tlv_stream).write(&mut bytes).unwrap(); diff --git a/lightning/src/offers/static_invoice.rs b/lightning/src/offers/static_invoice.rs index a2ea4032c64..56ef40aab73 100644 --- a/lightning/src/offers/static_invoice.rs +++ b/lightning/src/offers/static_invoice.rs @@ -16,7 +16,8 @@ use crate::ln::features::{Bolt12InvoiceFeatures, OfferFeatures}; 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, }; use crate::offers::invoice_macros::{invoice_accessors_common, invoice_builder_methods_common}; @@ -278,14 +279,17 @@ 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 (_, 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, + offer_bytes.len() + + invoice_tlv_stream.serialized_length() + + SIGNATURE_TLV_RECORD_SIZE + + experimental_invoice_tlv_stream.serialized_length(), ); // Use the offer bytes instead of the offer TLV stream as the latter may have contained @@ -304,13 +308,15 @@ impl UnsignedStaticInvoice { remaining_bytes.len() - experimental_tlv_stream .peek() - .map_or(remaining_bytes.len(), |first_record| first_record.start), + .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)); @@ -445,9 +451,11 @@ impl InvoiceContents { payment_hash: None, }; + let experimental_invoice = ExperimentalInvoiceTlvStreamRef {}; + let (offer, experimental_offer) = self.offer.as_tlv_stream(); - (offer, invoice, experimental_offer) + (offer, invoice, experimental_offer, experimental_invoice) } fn chain(&self) -> ChainHash { @@ -544,8 +552,13 @@ impl TryFrom> for StaticInvoice { } } -type FullInvoiceTlvStream = - (OfferTlvStream, InvoiceTlvStream, SignatureTlvStream, ExperimentalOfferTlvStream); +type FullInvoiceTlvStream = ( + OfferTlvStream, + InvoiceTlvStream, + SignatureTlvStream, + ExperimentalOfferTlvStream, + ExperimentalInvoiceTlvStream, +); impl CursorReadable for FullInvoiceTlvStream { fn read>(r: &mut io::Cursor) -> Result { @@ -553,15 +566,21 @@ impl CursorReadable for FullInvoiceTlvStream { 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, experimental_offer)) + Ok((offer, invoice, signature, experimental_offer, experimental_invoice)) } } -type PartialInvoiceTlvStream = (OfferTlvStream, InvoiceTlvStream, ExperimentalOfferTlvStream); +type PartialInvoiceTlvStream = + (OfferTlvStream, InvoiceTlvStream, ExperimentalOfferTlvStream, ExperimentalInvoiceTlvStream); -type PartialInvoiceTlvStreamRef<'a> = - (OfferTlvStreamRef<'a>, InvoiceTlvStreamRef<'a>, ExperimentalOfferTlvStreamRef); +type PartialInvoiceTlvStreamRef<'a> = ( + OfferTlvStreamRef<'a>, + InvoiceTlvStreamRef<'a>, + ExperimentalOfferTlvStreamRef, + ExperimentalInvoiceTlvStreamRef, +); impl TryFrom> for StaticInvoice { type Error = Bolt12ParseError; @@ -573,11 +592,13 @@ impl TryFrom> for StaticInvoice { 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 { @@ -615,6 +636,7 @@ impl TryFrom for InvoiceContents { amount, }, experimental_offer_tlv_stream, + ExperimentalInvoiceTlvStream {}, ) = tlv_stream; if payment_hash.is_some() { @@ -666,7 +688,9 @@ mod tests { use crate::ln::features::{Bolt12InvoiceFeatures, OfferFeatures}; use crate::ln::inbound_payment::ExpandedKey; use crate::ln::msgs::DecodeError; - use crate::offers::invoice::{InvoiceTlvStreamRef, INVOICE_TYPES}; + use crate::offers::invoice::{ + ExperimentalInvoiceTlvStreamRef, InvoiceTlvStreamRef, INVOICE_TYPES, + }; use crate::offers::merkle; use crate::offers::merkle::{SignatureTlvStreamRef, TaggedHash}; use crate::offers::nonce::Nonce; @@ -691,17 +715,23 @@ mod tests { InvoiceTlvStreamRef<'a>, SignatureTlvStreamRef<'a>, ExperimentalOfferTlvStreamRef, + ExperimentalInvoiceTlvStreamRef, ); impl StaticInvoice { fn as_tlv_stream(&self) -> FullInvoiceTlvStreamRef { - let (offer_tlv_stream, invoice_tlv_stream, experimental_offer_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, ) } } @@ -712,6 +742,7 @@ mod tests { InvoiceTlvStreamRef, SignatureTlvStreamRef, ExperimentalOfferTlvStreamRef, + ExperimentalInvoiceTlvStreamRef, ), ) -> Vec { let mut buffer = Vec::new(); @@ -719,6 +750,7 @@ mod tests { 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 } @@ -849,6 +881,7 @@ mod tests { }, SignatureTlvStreamRef { signature: Some(&invoice.signature()) }, ExperimentalOfferTlvStreamRef { experimental_foo: None }, + ExperimentalInvoiceTlvStreamRef {}, ) ); diff --git a/lightning/src/util/ser.rs b/lightning/src/util/ser.rs index a3b56fa8494..2c88f913433 100644 --- a/lightning/src/util/ser.rs +++ b/lightning/src/util/ser.rs @@ -1488,6 +1488,30 @@ impl Readable for (A, B, C, D, E, F, G) { + 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)?; + let e: E = Readable::read(r)?; + let f: F = Readable::read(r)?; + let g: G = Readable::read(r)?; + Ok((a, b, c, d, e, f, g)) + } +} +impl Writeable for (A, B, C, D, E, F, G) { + 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)?; + self.4.write(w)?; + self.5.write(w)?; + self.6.write(w) + } +} + impl Writeable for () { fn write(&self, _: &mut W) -> Result<(), io::Error> { Ok(()) From 76682409d4b11c25e23f207fa64e065576ec3f23 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Tue, 13 Aug 2024 17:32:28 -0500 Subject: [PATCH 18/19] Add parsing tests for experimental invoice TLVs --- lightning/src/offers/invoice.rs | 167 ++++++++++++++++++++- lightning/src/offers/invoice_macros.rs | 5 + lightning/src/offers/invoice_request.rs | 2 + lightning/src/offers/refund.rs | 2 + lightning/src/offers/static_invoice.rs | 184 +++++++++++++++++++++++- 5 files changed, 348 insertions(+), 12 deletions(-) diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index 08c4c6a78b7..ab5ff78984c 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -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, } } @@ -666,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) => { @@ -1256,7 +1260,10 @@ impl InvoiceFields { node_id: Some(&self.signing_pubkey), message_paths: None, }, - ExperimentalInvoiceTlvStreamRef {}, + ExperimentalInvoiceTlvStreamRef { + #[cfg(test)] + experimental_baz: self.experimental_baz, + }, ) } } @@ -1333,12 +1340,20 @@ tlv_stream!(InvoiceTlvStream, InvoiceTlvStreamRef<'a>, INVOICE_TYPES, { }); /// Valid type range for experimental invoice TLV records. -const EXPERIMENTAL_INVOICE_TYPES: core::ops::RangeFrom = 3_000_000_000..; +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, @@ -1473,7 +1488,10 @@ impl TryFrom for InvoiceContents { }, experimental_offer_tlv_stream, experimental_invoice_request_tlv_stream, - ExperimentalInvoiceTlvStream {}, + ExperimentalInvoiceTlvStream { + #[cfg(test)] + experimental_baz, + }, ) = tlv_stream; if message_paths.is_some() { return Err(Bolt12SemanticError::UnexpectedPaths) } @@ -1500,6 +1518,8 @@ 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)?; @@ -1570,7 +1590,7 @@ pub(super) fn check_invoice_signing_pubkey( #[cfg(test)] mod tests { - use super::{Bolt12Invoice, DEFAULT_RELATIVE_EXPIRY, ExperimentalInvoiceTlvStreamRef, FallbackAddress, FullInvoiceTlvStreamRef, INVOICE_TYPES, 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; @@ -1590,7 +1610,7 @@ mod tests { use crate::ln::inbound_payment::ExpandedKey; use crate::ln::msgs::DecodeError; use crate::offers::invoice_request::{ExperimentalInvoiceRequestTlvStreamRef, InvoiceRequestTlvStreamRef}; - 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, ExperimentalOfferTlvStreamRef, OfferTlvStreamRef, Quantity}; use crate::prelude::*; @@ -1766,7 +1786,9 @@ mod tests { ExperimentalInvoiceRequestTlvStreamRef { experimental_bar: None, }, - ExperimentalInvoiceTlvStreamRef {}, + ExperimentalInvoiceTlvStreamRef { + experimental_baz: None, + }, ), ); @@ -1866,7 +1888,9 @@ mod tests { ExperimentalInvoiceRequestTlvStreamRef { experimental_bar: None, }, - ExperimentalInvoiceTlvStreamRef {}, + ExperimentalInvoiceTlvStreamRef { + experimental_baz: None, + }, ), ); @@ -2724,6 +2748,135 @@ mod tests { } } + #[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()) 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 55fe9af1102..3c369feaceb 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -1551,6 +1551,7 @@ mod tests { 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) { @@ -1643,6 +1644,7 @@ mod tests { 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()); diff --git a/lightning/src/offers/refund.rs b/lightning/src/offers/refund.rs index fd0f0916f76..e538ab77f45 100644 --- a/lightning/src/offers/refund.rs +++ b/lightning/src/offers/refund.rs @@ -1110,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) { @@ -1178,6 +1179,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(); assert!(invoice.verify_using_metadata(&expanded_key, &secp_ctx).is_err()); diff --git a/lightning/src/offers/static_invoice.rs b/lightning/src/offers/static_invoice.rs index 56ef40aab73..b3618ae5d98 100644 --- a/lightning/src/offers/static_invoice.rs +++ b/lightning/src/offers/static_invoice.rs @@ -20,6 +20,8 @@ use crate::offers::invoice::{ 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::{ @@ -82,6 +84,8 @@ struct InvoiceContents { features: Bolt12InvoiceFeatures, signing_pubkey: PublicKey, message_paths: Vec, + #[cfg(test)] + experimental_baz: Option, } /// Builds a [`StaticInvoice`] from an [`Offer`]. @@ -168,6 +172,9 @@ 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. @@ -426,6 +433,8 @@ impl InvoiceContents { fallbacks: None, features: Bolt12InvoiceFeatures::empty(), signing_pubkey, + #[cfg(test)] + experimental_baz: None, } } @@ -451,7 +460,10 @@ impl InvoiceContents { payment_hash: None, }; - let experimental_invoice = ExperimentalInvoiceTlvStreamRef {}; + let experimental_invoice = ExperimentalInvoiceTlvStreamRef { + #[cfg(test)] + experimental_baz: self.experimental_baz, + }; let (offer, experimental_offer) = self.offer.as_tlv_stream(); @@ -636,7 +648,10 @@ impl TryFrom for InvoiceContents { amount, }, experimental_offer_tlv_stream, - ExperimentalInvoiceTlvStream {}, + ExperimentalInvoiceTlvStream { + #[cfg(test)] + experimental_baz, + }, ) = tlv_stream; if payment_hash.is_some() { @@ -677,6 +692,8 @@ impl TryFrom for InvoiceContents { fallbacks, features, signing_pubkey, + #[cfg(test)] + experimental_baz, }) } } @@ -689,10 +706,11 @@ mod tests { use crate::ln::inbound_payment::ExpandedKey; use crate::ln::msgs::DecodeError; use crate::offers::invoice::{ - ExperimentalInvoiceTlvStreamRef, InvoiceTlvStreamRef, INVOICE_TYPES, + 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::{ ExperimentalOfferTlvStreamRef, Offer, OfferBuilder, OfferTlvStreamRef, Quantity, @@ -881,7 +899,7 @@ mod tests { }, SignatureTlvStreamRef { signature: Some(&invoice.signature()) }, ExperimentalOfferTlvStreamRef { experimental_foo: None }, - ExperimentalInvoiceTlvStreamRef {}, + ExperimentalInvoiceTlvStreamRef { experimental_baz: None }, ) ); @@ -1439,6 +1457,162 @@ mod tests { } } + #[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(); From 169b2604935f4992528346bbd5e8b393d58227c5 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Mon, 21 Oct 2024 14:43:12 -0500 Subject: [PATCH 19/19] Refactor tuple serialization into a macro --- lightning/src/util/ser.rs | 137 ++++++++------------------------------ 1 file changed, 28 insertions(+), 109 deletions(-) diff --git a/lightning/src/util/ser.rs b/lightning/src/util/ser.rs index 2c88f913433..73dde00e786 100644 --- a/lightning/src/util/ser.rs +++ b/lightning/src/util/ser.rs @@ -1398,119 +1398,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) - } -} - -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 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 Readable for (A, B, C, D, E) { - 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)?; - let e: E = Readable::read(r)?; - Ok((a, b, c, d, e)) - } -} -impl Writeable for (A, B, C, D, E) { - 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)?; - self.4.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, D, E, F) { - 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)?; - let e: E = Readable::read(r)?; - let f: F = Readable::read(r)?; - Ok((a, b, c, d, e, f)) - } -} -impl Writeable for (A, B, C, D, E, F) { - 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)?; - self.4.write(w)?; - self.5.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, E, F, G) { - 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)?; - let e: E = Readable::read(r)?; - let f: F = Readable::read(r)?; - let g: G = Readable::read(r)?; - Ok((a, b, c, d, e, f, g)) - } -} -impl Writeable for (A, B, C, D, E, F, G) { - 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)?; - self.4.write(w)?; - self.5.write(w)?; - self.6.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> {