From: Jeffrey Czyz Date: Tue, 13 Aug 2024 22:32:28 +0000 (-0500) Subject: Add parsing tests for experimental invoice TLVs X-Git-Url: http://git.bitcoin.ninja/?a=commitdiff_plain;h=76682409d4b11c25e23f207fa64e065576ec3f23;p=rust-lightning Add parsing tests for experimental invoice TLVs --- diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index 08c4c6a78..ab5ff7898 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 579ecd2d2..4a540c160 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 55fe9af11..3c369feac 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 fd0f0916f..e538ab77f 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 56ef40aab..b3618ae5d 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();