From 1d15a6faf0422d1159e7c1e171e9854e6c9ddb90 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Tue, 13 Aug 2024 17:32:28 -0500 Subject: [PATCH] Add parsing tests for experimental invoice TLVs --- lightning/src/offers/invoice.rs | 136 +++++++++++++++++++++-- lightning/src/offers/invoice_macros.rs | 14 +++ lightning/src/offers/invoice_request.rs | 2 + lightning/src/offers/refund.rs | 2 + lightning/src/offers/static_invoice.rs | 140 +++++++++++++++++++++++- 5 files changed, 282 insertions(+), 12 deletions(-) diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index fd98d5b293c..ff15386d6b4 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -117,6 +117,8 @@ use crate::ln::features::{BlindedHopFeatures, Bolt12InvoiceFeatures, InvoiceRequ use crate::ln::inbound_payment::{ExpandedKey, IV_LEN}; 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_test_methods; 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}; use crate::offers::nonce::Nonce; @@ -359,6 +361,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, } } @@ -385,6 +389,9 @@ impl<'a> InvoiceBuilder<'a, DerivedSigningPubkey> { impl<'a, S: SigningPubkeyStrategy> InvoiceBuilder<'a, S> { invoice_builder_methods!(self, Self, Self, self, S, mut); invoice_builder_methods_common!(self, Self, self.invoice.fields_mut(), Self, self, S, Bolt12Invoice, mut); + + #[cfg(test)] + invoice_builder_test_methods!(self, Self, self.invoice.fields_mut(), Self, self, mut); } #[cfg(all(c_bindings, not(test)))] @@ -399,6 +406,7 @@ impl<'a> InvoiceWithExplicitSigningPubkeyBuilder<'a> { invoice_explicit_signing_pubkey_builder_methods!(self, &mut Self); invoice_builder_methods!(self, &mut Self, &mut Self, self, ExplicitSigningPubkey); invoice_builder_methods_common!(self, &mut Self, self.invoice.fields_mut(), &mut Self, self, ExplicitSigningPubkey, Bolt12Invoice); + invoice_builder_test_methods!(self, &mut Self, self.invoice.fields_mut(), &mut Self, self); } #[cfg(all(c_bindings, not(test)))] @@ -413,6 +421,7 @@ impl<'a> InvoiceWithDerivedSigningPubkeyBuilder<'a> { invoice_derived_signing_pubkey_builder_methods!(self, &mut Self); invoice_builder_methods!(self, &mut Self, &mut Self, self, DerivedSigningPubkey); invoice_builder_methods_common!(self, &mut Self, self.invoice.fields_mut(), &mut Self, self, DerivedSigningPubkey, Bolt12Invoice); + invoice_builder_test_methods!(self, &mut Self, self.invoice.fields_mut(), &mut Self, self); } #[cfg(c_bindings)] @@ -624,6 +633,8 @@ struct InvoiceFields { fallbacks: Option>, features: Bolt12InvoiceFeatures, signing_pubkey: PublicKey, + #[cfg(test)] + experimental_baz: Option, } macro_rules! invoice_accessors { ($self: ident, $contents: expr) => { @@ -1208,7 +1219,10 @@ impl InvoiceFields { node_id: Some(&self.signing_pubkey), message_paths: None, }, - ExperimentalInvoiceTlvStreamRef {}, + ExperimentalInvoiceTlvStreamRef { + #[cfg(test)] + experimental_baz: self.experimental_baz, + }, ) } } @@ -1297,12 +1311,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, (BlindedPayInfo, BlindedPath)>, for<'r> fn(&'r (BlindedPayInfo, BlindedPath)) -> &'r BlindedPath, @@ -1475,7 +1497,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) } @@ -1502,6 +1527,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)?; @@ -1567,7 +1594,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::{WitnessProgram, WitnessVersion}; use bitcoin::blockdata::constants::ChainHash; @@ -1586,7 +1613,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::*; @@ -1762,7 +1789,9 @@ mod tests { ExperimentalInvoiceRequestTlvStreamRef { experimental_bar: None, }, - ExperimentalInvoiceTlvStreamRef {}, + ExperimentalInvoiceTlvStreamRef { + experimental_baz: None, + }, ), ); @@ -1862,7 +1891,9 @@ mod tests { ExperimentalInvoiceRequestTlvStreamRef { experimental_bar: None, }, - ExperimentalInvoiceTlvStreamRef {}, + ExperimentalInvoiceTlvStreamRef { + experimental_baz: None, + }, ), ); @@ -2713,6 +2744,97 @@ 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 buffer = Vec::new(); + invoice.write(&mut buffer).unwrap(); + + assert!(Bolt12Invoice::try_from(buffer).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(); + + BigSize(UNKNOWN_ODD_TYPE).write(&mut unsigned_invoice.experimental_bytes).unwrap(); + BigSize(32).write(&mut unsigned_invoice.experimental_bytes).unwrap(); + [42u8; 32].write(&mut unsigned_invoice.experimental_bytes).unwrap(); + + 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(); + + if let Err(e) = Bolt12Invoice::try_from(encoded_invoice) { + 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(); + + BigSize(UNKNOWN_EVEN_TYPE).write(&mut unsigned_invoice.experimental_bytes).unwrap(); + BigSize(32).write(&mut unsigned_invoice.experimental_bytes).unwrap(); + [42u8; 32].write(&mut unsigned_invoice.experimental_bytes).unwrap(); + + 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)), + } + } + #[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 0ac396ae0a8..3ca9f6b35c7 100644 --- a/lightning/src/offers/invoice_macros.rs +++ b/lightning/src/offers/invoice_macros.rs @@ -126,5 +126,19 @@ macro_rules! invoice_accessors_common { ($self: ident, $contents: expr, $invoice } } } +#[cfg(test)] +macro_rules! invoice_builder_test_methods { ( + $self: ident, $self_type: ty, $invoice_fields: expr, $return_type: ty, $return_value: expr + $(, $self_mut: tt)? +) => { + #[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 + } +} } + pub(super) use invoice_accessors_common; pub(super) use invoice_builder_methods_common; +#[cfg(test)] +pub(super) use invoice_builder_test_methods; diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index d12dcfa3d08..610f7108c6a 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -1513,6 +1513,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) { @@ -1605,6 +1606,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 74afcbdec9d..6a44068a557 100644 --- a/lightning/src/offers/refund.rs +++ b/lightning/src/offers/refund.rs @@ -1109,6 +1109,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 25cab93cbce..d5ef1416a6f 100644 --- a/lightning/src/offers/static_invoice.rs +++ b/lightning/src/offers/static_invoice.rs @@ -19,6 +19,8 @@ use crate::offers::invoice::{ ExperimentalInvoiceTlvStream, ExperimentalInvoiceTlvStreamRef, FallbackAddress, InvoiceTlvStream, InvoiceTlvStreamRef, }; +#[cfg(test)] +use crate::offers::invoice_macros::invoice_builder_test_methods; use crate::offers::invoice_macros::{invoice_accessors_common, invoice_builder_methods_common}; use crate::offers::merkle::{ self, SignError, SignFn, SignatureTlvStream, SignatureTlvStreamRef, TaggedHash, TlvStream, @@ -79,6 +81,8 @@ struct InvoiceContents { features: Bolt12InvoiceFeatures, signing_pubkey: PublicKey, message_paths: Vec, + #[cfg(test)] + experimental_baz: Option, } /// Builds a [`StaticInvoice`] from an [`Offer`]. @@ -163,6 +167,9 @@ impl<'a> StaticInvoiceBuilder<'a> { } invoice_builder_methods_common!(self, Self, self.invoice, Self, self, S, StaticInvoice, mut); + + #[cfg(test)] + invoice_builder_test_methods!(self, Self, self.invoice, Self, self, mut); } /// A semantically valid [`StaticInvoice`] that hasn't been signed. @@ -389,6 +396,8 @@ impl InvoiceContents { fallbacks: None, features: Bolt12InvoiceFeatures::empty(), signing_pubkey, + #[cfg(test)] + experimental_baz: None, } } @@ -414,7 +423,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(); @@ -599,7 +611,10 @@ impl TryFrom for InvoiceContents { amount, }, experimental_offer_tlv_stream, - ExperimentalInvoiceTlvStream {}, + ExperimentalInvoiceTlvStream { + #[cfg(test)] + experimental_baz, + }, ) = tlv_stream; if payment_hash.is_some() { @@ -640,6 +655,8 @@ impl TryFrom for InvoiceContents { fallbacks, features, signing_pubkey, + #[cfg(test)] + experimental_baz, }) } } @@ -650,9 +667,12 @@ mod tests { use crate::ln::features::{Bolt12InvoiceFeatures, OfferFeatures}; use crate::ln::inbound_payment::ExpandedKey; use crate::ln::msgs::DecodeError; - use crate::offers::invoice::{ExperimentalInvoiceTlvStreamRef, InvoiceTlvStreamRef, INVOICE_TYPES}; + use crate::offers::invoice::{ + ExperimentalInvoiceTlvStreamRef, InvoiceTlvStreamRef, EXPERIMENTAL_INVOICE_TYPES, + INVOICE_TYPES, + }; use crate::offers::merkle; - use crate::offers::merkle::{SignatureTlvStreamRef, TaggedHash}; + use crate::offers::merkle::{SignatureTlvStreamRef, TaggedHash, TlvStream}; use crate::offers::nonce::Nonce; use crate::offers::offer::{ ExperimentalOfferTlvStreamRef, Offer, OfferBuilder, OfferTlvStreamRef, Quantity, @@ -839,7 +859,7 @@ mod tests { }, SignatureTlvStreamRef { signature: Some(&invoice.signature()) }, ExperimentalOfferTlvStreamRef { experimental_foo: None }, - ExperimentalInvoiceTlvStreamRef {}, + ExperimentalInvoiceTlvStreamRef { experimental_baz: None }, ) ); @@ -1386,6 +1406,116 @@ 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 buffer = Vec::new(); + invoice.write(&mut buffer).unwrap(); + + assert!(StaticInvoice::try_from(buffer).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(); + + BigSize(UNKNOWN_ODD_TYPE).write(&mut unsigned_invoice.experimental_bytes).unwrap(); + BigSize(32).write(&mut unsigned_invoice.experimental_bytes).unwrap(); + [42u8; 32].write(&mut unsigned_invoice.experimental_bytes).unwrap(); + + 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(); + + if let Err(e) = StaticInvoice::try_from(encoded_invoice) { + 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(); + + BigSize(UNKNOWN_EVEN_TYPE).write(&mut unsigned_invoice.experimental_bytes).unwrap(); + BigSize(32).write(&mut unsigned_invoice.experimental_bytes).unwrap(); + [42u8; 32].write(&mut unsigned_invoice.experimental_bytes).unwrap(); + + 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)), + } + } + #[test] fn fails_parsing_invoice_with_out_of_range_tlv_records() { let invoice = invoice();