From f18b490d8775d3395e937330daa4b470e5e33085 Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Thu, 23 May 2024 11:00:40 -0400 Subject: [PATCH 01/12] Remove redundant checks in BOLT 12 invoice tests. --- lightning/src/offers/invoice.rs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index 5482e6f83..4206e67d2 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -1566,10 +1566,8 @@ mod tests { #[cfg(feature = "std")] assert!(!unsigned_invoice.is_expired()); assert_eq!(unsigned_invoice.payment_hash(), payment_hash); - assert_eq!(unsigned_invoice.amount_msats(), 1000); assert!(unsigned_invoice.fallbacks().is_empty()); assert_eq!(unsigned_invoice.invoice_features(), &Bolt12InvoiceFeatures::empty()); - assert_eq!(unsigned_invoice.signing_pubkey(), recipient_pubkey()); match UnsignedBolt12Invoice::try_from(buffer) { Err(e) => panic!("error parsing unsigned invoice: {:?}", e), @@ -1610,10 +1608,8 @@ mod tests { #[cfg(feature = "std")] assert!(!invoice.is_expired()); assert_eq!(invoice.payment_hash(), payment_hash); - assert_eq!(invoice.amount_msats(), 1000); assert!(invoice.fallbacks().is_empty()); assert_eq!(invoice.invoice_features(), &Bolt12InvoiceFeatures::empty()); - assert_eq!(invoice.signing_pubkey(), recipient_pubkey()); let message = TaggedHash::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &invoice.bytes); assert!(merkle::verify_signature(&invoice.signature, &message, recipient_pubkey()).is_ok()); @@ -1708,10 +1704,8 @@ mod tests { #[cfg(feature = "std")] assert!(!invoice.is_expired()); assert_eq!(invoice.payment_hash(), payment_hash); - assert_eq!(invoice.amount_msats(), 1000); assert!(invoice.fallbacks().is_empty()); assert_eq!(invoice.invoice_features(), &Bolt12InvoiceFeatures::empty()); - assert_eq!(invoice.signing_pubkey(), recipient_pubkey()); let message = TaggedHash::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &invoice.bytes); assert!(merkle::verify_signature(&invoice.signature, &message, recipient_pubkey()).is_ok()); From 45d7d2d95ee82fb5e9cccc028918dd49e664c673 Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Fri, 19 Apr 2024 16:04:38 -0400 Subject: [PATCH 02/12] Move common BOLT 12 invoice builder setters to new macro. Will be useful when we later add support for static BOLT 12 invoices. --- lightning/src/offers/invoice.rs | 68 +++------------------ lightning/src/offers/invoice_macros.rs | 83 ++++++++++++++++++++++++++ lightning/src/offers/mod.rs | 1 + 3 files changed, 91 insertions(+), 61 deletions(-) create mode 100644 lightning/src/offers/invoice_macros.rs diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index 4206e67d2..4fcd38a1c 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -102,12 +102,11 @@ //! //! ``` -use bitcoin::{WitnessProgram, Network, WitnessVersion, WPubkeyHash, WScriptHash}; +use bitcoin::{WitnessProgram, Network, WitnessVersion}; use bitcoin::blockdata::constants::ChainHash; use bitcoin::secp256k1::{Keypair, PublicKey, Secp256k1, self}; use bitcoin::secp256k1::schnorr::Signature; use bitcoin::address::{Address, Payload}; -use bitcoin::key::TweakedPublicKey; use core::time::Duration; use core::hash::{Hash, Hasher}; use crate::io; @@ -117,6 +116,7 @@ use crate::ln::channelmanager::PaymentId; use crate::ln::features::{BlindedHopFeatures, Bolt12InvoiceFeatures, InvoiceRequestFeatures, OfferFeatures}; use crate::ln::inbound_payment::ExpandedKey; use crate::ln::msgs::DecodeError; +use crate::offers::invoice_macros::invoice_builder_methods_common; 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::offer::{Amount, OFFER_TYPES, OfferTlvStream, OfferTlvStreamRef, Quantity}; @@ -371,65 +371,6 @@ macro_rules! invoice_builder_methods { ( Ok(Self { invreq_bytes, invoice: contents, signing_pubkey_strategy }) } - - /// Sets the [`Bolt12Invoice::relative_expiry`] as seconds since [`Bolt12Invoice::created_at`]. - /// Any expiry that has already passed is valid and can be checked for using - /// [`Bolt12Invoice::is_expired`]. - /// - /// Successive calls to this method will override the previous setting. - pub fn relative_expiry($($self_mut)* $self: $self_type, relative_expiry_secs: u32) -> $return_type { - let relative_expiry = Duration::from_secs(relative_expiry_secs as u64); - $self.invoice.fields_mut().relative_expiry = Some(relative_expiry); - $return_value - } - - /// Adds a P2WSH address to [`Bolt12Invoice::fallbacks`]. - /// - /// Successive calls to this method will add another address. Caller is responsible for not - /// adding duplicate addresses and only calling if capable of receiving to P2WSH addresses. - pub fn fallback_v0_p2wsh($($self_mut)* $self: $self_type, script_hash: &WScriptHash) -> $return_type { - use bitcoin::hashes::Hash; - let address = FallbackAddress { - version: WitnessVersion::V0.to_num(), - program: Vec::from(script_hash.to_byte_array()), - }; - $self.invoice.fields_mut().fallbacks.get_or_insert_with(Vec::new).push(address); - $return_value - } - - /// Adds a P2WPKH address to [`Bolt12Invoice::fallbacks`]. - /// - /// Successive calls to this method will add another address. Caller is responsible for not - /// adding duplicate addresses and only calling if capable of receiving to P2WPKH addresses. - pub fn fallback_v0_p2wpkh($($self_mut)* $self: $self_type, pubkey_hash: &WPubkeyHash) -> $return_type { - use bitcoin::hashes::Hash; - let address = FallbackAddress { - version: WitnessVersion::V0.to_num(), - program: Vec::from(pubkey_hash.to_byte_array()), - }; - $self.invoice.fields_mut().fallbacks.get_or_insert_with(Vec::new).push(address); - $return_value - } - - /// Adds a P2TR address to [`Bolt12Invoice::fallbacks`]. - /// - /// Successive calls to this method will add another address. Caller is responsible for not - /// adding duplicate addresses and only calling if capable of receiving to P2TR addresses. - pub fn fallback_v1_p2tr_tweaked($($self_mut)* $self: $self_type, output_key: &TweakedPublicKey) -> $return_type { - let address = FallbackAddress { - version: WitnessVersion::V1.to_num(), - program: Vec::from(&output_key.serialize()[..]), - }; - $self.invoice.fields_mut().fallbacks.get_or_insert_with(Vec::new).push(address); - $return_value - } - - /// Sets [`Bolt12Invoice::invoice_features`] to indicate MPP may be used. Otherwise, MPP is - /// disallowed. - pub fn allow_mpp($($self_mut)* $self: $self_type) -> $return_type { - $self.invoice.fields_mut().features.set_basic_mpp_optional(); - $return_value - } } } impl<'a> InvoiceBuilder<'a, ExplicitSigningPubkey> { @@ -442,30 +383,35 @@ 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, mut); } #[cfg(all(c_bindings, not(test)))] impl<'a> InvoiceWithExplicitSigningPubkeyBuilder<'a> { invoice_explicit_signing_pubkey_builder_methods!(self, &mut Self); invoice_builder_methods!(self, &mut Self, (), (), ExplicitSigningPubkey); + invoice_builder_methods_common!(self, &mut Self, self.invoice.fields_mut(), (), (), ExplicitSigningPubkey); } #[cfg(all(c_bindings, test))] 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); } #[cfg(all(c_bindings, not(test)))] impl<'a> InvoiceWithDerivedSigningPubkeyBuilder<'a> { invoice_derived_signing_pubkey_builder_methods!(self, &mut Self); invoice_builder_methods!(self, &mut Self, (), (), DerivedSigningPubkey); + invoice_builder_methods_common!(self, &mut Self, self.invoice.fields_mut(), (), (), DerivedSigningPubkey); } #[cfg(all(c_bindings, test))] 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); } #[cfg(c_bindings)] diff --git a/lightning/src/offers/invoice_macros.rs b/lightning/src/offers/invoice_macros.rs new file mode 100644 index 000000000..c1e860b46 --- /dev/null +++ b/lightning/src/offers/invoice_macros.rs @@ -0,0 +1,83 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! Shared code between BOLT 12 static and single-use invoices. + +macro_rules! invoice_builder_methods_common { ( + $self: ident, $self_type: ty, $invoice_fields: expr, $return_type: ty, $return_value: expr, $type_param: ty $(, $self_mut: tt)? +) => { + /// Sets the [`Bolt12Invoice::relative_expiry`] as seconds since [`Bolt12Invoice::created_at`]. + /// Any expiry that has already passed is valid and can be checked for using + /// [`Bolt12Invoice::is_expired`]. + /// + /// Successive calls to this method will override the previous setting. + pub fn relative_expiry($($self_mut)* $self: $self_type, relative_expiry_secs: u32) -> $return_type { + let relative_expiry = Duration::from_secs(relative_expiry_secs as u64); + $invoice_fields.relative_expiry = Some(relative_expiry); + $return_value + } + + /// Adds a P2WSH address to [`Bolt12Invoice::fallbacks`]. + /// + /// Successive calls to this method will add another address. Caller is responsible for not + /// adding duplicate addresses and only calling if capable of receiving to P2WSH addresses. + pub fn fallback_v0_p2wsh( + $($self_mut)* $self: $self_type, script_hash: &bitcoin::WScriptHash + ) -> $return_type { + use bitcoin::hashes::Hash; + + let address = FallbackAddress { + version: bitcoin::WitnessVersion::V0.to_num(), + program: Vec::from(script_hash.to_byte_array()), + }; + $invoice_fields.fallbacks.get_or_insert_with(Vec::new).push(address); + $return_value + } + + /// Adds a P2WPKH address to [`Bolt12Invoice::fallbacks`]. + /// + /// Successive calls to this method will add another address. Caller is responsible for not + /// adding duplicate addresses and only calling if capable of receiving to P2WPKH addresses. + pub fn fallback_v0_p2wpkh( + $($self_mut)* $self: $self_type, pubkey_hash: &bitcoin::WPubkeyHash + ) -> $return_type { + use bitcoin::hashes::Hash; + + let address = FallbackAddress { + version: bitcoin::WitnessVersion::V0.to_num(), + program: Vec::from(pubkey_hash.to_byte_array()), + }; + $invoice_fields.fallbacks.get_or_insert_with(Vec::new).push(address); + $return_value + } + + /// Adds a P2TR address to [`Bolt12Invoice::fallbacks`]. + /// + /// Successive calls to this method will add another address. Caller is responsible for not + /// adding duplicate addresses and only calling if capable of receiving to P2TR addresses. + pub fn fallback_v1_p2tr_tweaked( + $($self_mut)* $self: $self_type, output_key: &bitcoin::key::TweakedPublicKey + ) -> $return_type { + let address = FallbackAddress { + version: bitcoin::WitnessVersion::V1.to_num(), + program: Vec::from(&output_key.serialize()[..]), + }; + $invoice_fields.fallbacks.get_or_insert_with(Vec::new).push(address); + $return_value + } + + /// Sets [`Bolt12Invoice::invoice_features`] to indicate MPP may be used. Otherwise, MPP is + /// disallowed. + pub fn allow_mpp($($self_mut)* $self: $self_type) -> $return_type { + $invoice_fields.features.set_basic_mpp_optional(); + $return_value + } +} } + +pub(super) use invoice_builder_methods_common; diff --git a/lightning/src/offers/mod.rs b/lightning/src/offers/mod.rs index 3593b14f1..eb9b18e18 100644 --- a/lightning/src/offers/mod.rs +++ b/lightning/src/offers/mod.rs @@ -17,6 +17,7 @@ pub mod offer; pub mod invoice; pub mod invoice_error; +mod invoice_macros; pub mod invoice_request; pub mod merkle; pub mod parse; From 7417406676b0f2b6bfffaed25f5c906bab104d96 Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Thu, 9 May 2024 14:19:41 -0400 Subject: [PATCH 03/12] Move common BOLT 12 accessor methods to new macro. Will be useful when we support static BOLT 12 invoices. --- lightning/src/offers/invoice.rs | 49 ++----------------------- lightning/src/offers/invoice_macros.rs | 51 +++++++++++++++++++++++++- 2 files changed, 53 insertions(+), 47 deletions(-) diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index 4fcd38a1c..7fdbab337 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -116,7 +116,7 @@ use crate::ln::channelmanager::PaymentId; use crate::ln::features::{BlindedHopFeatures, Bolt12InvoiceFeatures, InvoiceRequestFeatures, OfferFeatures}; use crate::ln::inbound_payment::ExpandedKey; use crate::ln::msgs::DecodeError; -use crate::offers::invoice_macros::invoice_builder_methods_common; +use crate::offers::invoice_macros::{invoice_accessors_common, invoice_builder_methods_common}; 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::offer::{Amount, OFFER_TYPES, OfferTlvStream, OfferTlvStreamRef, Quantity}; @@ -740,35 +740,6 @@ macro_rules! invoice_accessors { ($self: ident, $contents: expr) => { $contents.payer_note() } - /// Paths to the recipient originating from publicly reachable nodes, including information - /// needed for routing payments across them. - /// - /// Blinded paths provide recipient privacy by obfuscating its node id. Note, however, that this - /// privacy is lost if a public node id is used for [`Bolt12Invoice::signing_pubkey`]. - /// - /// This is not exported to bindings users as slices with non-reference types cannot be ABI - /// matched in another language. - pub fn payment_paths(&$self) -> &[(BlindedPayInfo, BlindedPath)] { - $contents.payment_paths() - } - - /// Duration since the Unix epoch when the invoice was created. - pub fn created_at(&$self) -> Duration { - $contents.created_at() - } - - /// Duration since [`Bolt12Invoice::created_at`] when the invoice has expired and therefore - /// should no longer be paid. - pub fn relative_expiry(&$self) -> Duration { - $contents.relative_expiry() - } - - /// Whether the invoice has expired. - #[cfg(feature = "std")] - pub fn is_expired(&$self) -> bool { - $contents.is_expired() - } - /// SHA256 hash of the payment preimage that will be given in return for paying the invoice. pub fn payment_hash(&$self) -> PaymentHash { $contents.payment_hash() @@ -778,29 +749,15 @@ macro_rules! invoice_accessors { ($self: ident, $contents: expr) => { pub fn amount_msats(&$self) -> u64 { $contents.amount_msats() } - - /// Fallback addresses for paying the invoice on-chain, in order of most-preferred to - /// least-preferred. - pub fn fallbacks(&$self) -> Vec
{ - $contents.fallbacks() - } - - /// Features pertaining to paying an invoice. - pub fn invoice_features(&$self) -> &Bolt12InvoiceFeatures { - $contents.features() - } - - /// The public key corresponding to the key used to sign the invoice. - pub fn signing_pubkey(&$self) -> PublicKey { - $contents.signing_pubkey() - } } } impl UnsignedBolt12Invoice { + invoice_accessors_common!(self, self.contents); invoice_accessors!(self, self.contents); } impl Bolt12Invoice { + invoice_accessors_common!(self, self.contents); invoice_accessors!(self, self.contents); /// Signature of the invoice verified using [`Bolt12Invoice::signing_pubkey`]. diff --git a/lightning/src/offers/invoice_macros.rs b/lightning/src/offers/invoice_macros.rs index c1e860b46..8b32edcec 100644 --- a/lightning/src/offers/invoice_macros.rs +++ b/lightning/src/offers/invoice_macros.rs @@ -10,7 +10,8 @@ //! Shared code between BOLT 12 static and single-use invoices. macro_rules! invoice_builder_methods_common { ( - $self: ident, $self_type: ty, $invoice_fields: expr, $return_type: ty, $return_value: expr, $type_param: ty $(, $self_mut: tt)? + $self: ident, $self_type: ty, $invoice_fields: expr, $return_type: ty, $return_value: expr, + $type_param: ty $(, $self_mut: tt)? ) => { /// Sets the [`Bolt12Invoice::relative_expiry`] as seconds since [`Bolt12Invoice::created_at`]. /// Any expiry that has already passed is valid and can be checked for using @@ -80,4 +81,52 @@ macro_rules! invoice_builder_methods_common { ( } } } +macro_rules! invoice_accessors_common { ($self: ident, $contents: expr) => { + /// Paths to the recipient originating from publicly reachable nodes, including information + /// needed for routing payments across them. + /// + /// Blinded paths provide recipient privacy by obfuscating its node id. Note, however, that this + /// privacy is lost if a public node id is used for [`Bolt12Invoice::signing_pubkey`]. + /// + /// This is not exported to bindings users as slices with non-reference types cannot be ABI + /// matched in another language. + pub fn payment_paths(&$self) -> &[(BlindedPayInfo, BlindedPath)] { + $contents.payment_paths() + } + + /// Duration since the Unix epoch when the invoice was created. + pub fn created_at(&$self) -> Duration { + $contents.created_at() + } + + /// Duration since [`Bolt12Invoice::created_at`] when the invoice has expired and therefore + /// should no longer be paid. + pub fn relative_expiry(&$self) -> Duration { + $contents.relative_expiry() + } + + /// Whether the invoice has expired. + #[cfg(feature = "std")] + pub fn is_expired(&$self) -> bool { + $contents.is_expired() + } + + /// Fallback addresses for paying the invoice on-chain, in order of most-preferred to + /// least-preferred. + pub fn fallbacks(&$self) -> Vec
{ + $contents.fallbacks() + } + + /// Features pertaining to paying an invoice. + pub fn invoice_features(&$self) -> &Bolt12InvoiceFeatures { + $contents.features() + } + + /// The public key corresponding to the key used to sign the invoice. + pub fn signing_pubkey(&$self) -> PublicKey { + $contents.signing_pubkey() + } +} } + +pub(super) use invoice_accessors_common; pub(super) use invoice_builder_methods_common; From bbc15f56e92614dec33e5ada22008776d3e51571 Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Wed, 22 May 2024 16:13:10 -0400 Subject: [PATCH 04/12] Genericize BOLT 12 invoice{_builder} common macro docs over invoice type Will be useful so the docs generated work for static invoices. --- lightning/src/offers/invoice.rs | 14 ++++++------- lightning/src/offers/invoice_macros.rs | 29 ++++++++++++++------------ 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index 7fdbab337..ddda3b291 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -383,35 +383,35 @@ 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, mut); + invoice_builder_methods_common!(self, Self, self.invoice.fields_mut(), Self, self, S, Bolt12Invoice, mut); } #[cfg(all(c_bindings, not(test)))] impl<'a> InvoiceWithExplicitSigningPubkeyBuilder<'a> { invoice_explicit_signing_pubkey_builder_methods!(self, &mut Self); invoice_builder_methods!(self, &mut Self, (), (), ExplicitSigningPubkey); - invoice_builder_methods_common!(self, &mut Self, self.invoice.fields_mut(), (), (), ExplicitSigningPubkey); + invoice_builder_methods_common!(self, &mut Self, self.invoice.fields_mut(), (), (), ExplicitSigningPubkey, Bolt12Invoice); } #[cfg(all(c_bindings, test))] 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); + invoice_builder_methods_common!(self, &mut Self, self.invoice.fields_mut(), &mut Self, self, ExplicitSigningPubkey, Bolt12Invoice); } #[cfg(all(c_bindings, not(test)))] impl<'a> InvoiceWithDerivedSigningPubkeyBuilder<'a> { invoice_derived_signing_pubkey_builder_methods!(self, &mut Self); invoice_builder_methods!(self, &mut Self, (), (), DerivedSigningPubkey); - invoice_builder_methods_common!(self, &mut Self, self.invoice.fields_mut(), (), (), DerivedSigningPubkey); + invoice_builder_methods_common!(self, &mut Self, self.invoice.fields_mut(), (), (), DerivedSigningPubkey, Bolt12Invoice); } #[cfg(all(c_bindings, test))] 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); + invoice_builder_methods_common!(self, &mut Self, self.invoice.fields_mut(), &mut Self, self, DerivedSigningPubkey, Bolt12Invoice); } #[cfg(c_bindings)] @@ -752,12 +752,12 @@ macro_rules! invoice_accessors { ($self: ident, $contents: expr) => { } } impl UnsignedBolt12Invoice { - invoice_accessors_common!(self, self.contents); + invoice_accessors_common!(self, self.contents, Bolt12Invoice); invoice_accessors!(self, self.contents); } impl Bolt12Invoice { - invoice_accessors_common!(self, self.contents); + invoice_accessors_common!(self, self.contents, Bolt12Invoice); invoice_accessors!(self, self.contents); /// Signature of the invoice verified using [`Bolt12Invoice::signing_pubkey`]. diff --git a/lightning/src/offers/invoice_macros.rs b/lightning/src/offers/invoice_macros.rs index 8b32edcec..b79bb8c9e 100644 --- a/lightning/src/offers/invoice_macros.rs +++ b/lightning/src/offers/invoice_macros.rs @@ -11,11 +11,12 @@ macro_rules! invoice_builder_methods_common { ( $self: ident, $self_type: ty, $invoice_fields: expr, $return_type: ty, $return_value: expr, - $type_param: ty $(, $self_mut: tt)? + $type_param: ty, $invoice_type: ty $(, $self_mut: tt)? ) => { - /// Sets the [`Bolt12Invoice::relative_expiry`] as seconds since [`Bolt12Invoice::created_at`]. - /// Any expiry that has already passed is valid and can be checked for using - /// [`Bolt12Invoice::is_expired`]. + #[doc = concat!("Sets the [`", stringify!($invoice_type), "::relative_expiry`]")] + #[doc = concat!("as seconds since [`", stringify!($invoice_type), "::created_at`].")] + #[doc = "Any expiry that has already passed is valid and can be checked for using"] + #[doc = concat!("[`", stringify!($invoice_type), "::is_expired`].")] /// /// Successive calls to this method will override the previous setting. pub fn relative_expiry($($self_mut)* $self: $self_type, relative_expiry_secs: u32) -> $return_type { @@ -24,7 +25,7 @@ macro_rules! invoice_builder_methods_common { ( $return_value } - /// Adds a P2WSH address to [`Bolt12Invoice::fallbacks`]. + #[doc = concat!("Adds a P2WSH address to [`", stringify!($invoice_type), "::fallbacks`].")] /// /// Successive calls to this method will add another address. Caller is responsible for not /// adding duplicate addresses and only calling if capable of receiving to P2WSH addresses. @@ -41,7 +42,7 @@ macro_rules! invoice_builder_methods_common { ( $return_value } - /// Adds a P2WPKH address to [`Bolt12Invoice::fallbacks`]. + #[doc = concat!("Adds a P2WPKH address to [`", stringify!($invoice_type), "::fallbacks`].")] /// /// Successive calls to this method will add another address. Caller is responsible for not /// adding duplicate addresses and only calling if capable of receiving to P2WPKH addresses. @@ -58,7 +59,7 @@ macro_rules! invoice_builder_methods_common { ( $return_value } - /// Adds a P2TR address to [`Bolt12Invoice::fallbacks`]. + #[doc = concat!("Adds a P2TR address to [`", stringify!($invoice_type), "::fallbacks`].")] /// /// Successive calls to this method will add another address. Caller is responsible for not /// adding duplicate addresses and only calling if capable of receiving to P2TR addresses. @@ -73,20 +74,21 @@ macro_rules! invoice_builder_methods_common { ( $return_value } - /// Sets [`Bolt12Invoice::invoice_features`] to indicate MPP may be used. Otherwise, MPP is - /// disallowed. + #[doc = concat!("Sets [`", stringify!($invoice_type), "::invoice_features`]")] + #[doc = "to indicate MPP may be used. Otherwise, MPP is disallowed."] pub fn allow_mpp($($self_mut)* $self: $self_type) -> $return_type { $invoice_fields.features.set_basic_mpp_optional(); $return_value } } } -macro_rules! invoice_accessors_common { ($self: ident, $contents: expr) => { +macro_rules! invoice_accessors_common { ($self: ident, $contents: expr, $invoice_type: ty) => { /// Paths to the recipient originating from publicly reachable nodes, including information /// needed for routing payments across them. /// /// Blinded paths provide recipient privacy by obfuscating its node id. Note, however, that this - /// privacy is lost if a public node id is used for [`Bolt12Invoice::signing_pubkey`]. + /// privacy is lost if a public node id is used for + #[doc = concat!("[`", stringify!($invoice_type), "::signing_pubkey`].")] /// /// This is not exported to bindings users as slices with non-reference types cannot be ABI /// matched in another language. @@ -99,8 +101,9 @@ macro_rules! invoice_accessors_common { ($self: ident, $contents: expr) => { $contents.created_at() } - /// Duration since [`Bolt12Invoice::created_at`] when the invoice has expired and therefore - /// should no longer be paid. + /// Duration since + #[doc = concat!("[`", stringify!($invoice_type), "::created_at`]")] + /// when the invoice has expired and therefore should no longer be paid. pub fn relative_expiry(&$self) -> Duration { $contents.relative_expiry() } From b073711738ad4759a7b2991dc5e19c5c3905e5a3 Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Thu, 9 May 2024 14:23:40 -0400 Subject: [PATCH 05/12] BOLT 12 invoice: expose common helper methods and fields Useful for static invoice support. --- lightning/src/offers/invoice.rs | 120 ++++++++++++++++++-------------- 1 file changed, 68 insertions(+), 52 deletions(-) diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index ddda3b291..189033758 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -200,7 +200,7 @@ pub struct ExplicitSigningPubkey {} /// [`Bolt12Invoice::signing_pubkey`] was derived. /// /// This is not exported to bindings users as builder patterns don't map outside of move semantics. -pub struct DerivedSigningPubkey(Keypair); +pub struct DerivedSigningPubkey(pub(super) Keypair); impl SigningPubkeyStrategy for ExplicitSigningPubkey {} impl SigningPubkeyStrategy for DerivedSigningPubkey {} @@ -958,14 +958,7 @@ impl InvoiceContents { #[cfg(feature = "std")] fn is_expired(&self) -> bool { - let absolute_expiry = self.created_at().checked_add(self.relative_expiry()); - match absolute_expiry { - Some(seconds_from_epoch) => match SystemTime::UNIX_EPOCH.elapsed() { - Ok(elapsed) => elapsed > seconds_from_epoch, - Err(_) => false, - }, - None => false, - } + is_expired(self.created_at(), self.relative_expiry()) } fn payment_hash(&self) -> PaymentHash { @@ -977,36 +970,9 @@ impl InvoiceContents { } fn fallbacks(&self) -> Vec
{ - let chain = self.chain(); - let network = if chain == ChainHash::using_genesis_block(Network::Bitcoin) { - Network::Bitcoin - } else if chain == ChainHash::using_genesis_block(Network::Testnet) { - Network::Testnet - } else if chain == ChainHash::using_genesis_block(Network::Signet) { - Network::Signet - } else if chain == ChainHash::using_genesis_block(Network::Regtest) { - Network::Regtest - } else { - return Vec::new() - }; - - let to_valid_address = |address: &FallbackAddress| { - let version = match WitnessVersion::try_from(address.version) { - Ok(version) => version, - Err(_) => return None, - }; - - let program = address.program.clone(); - let witness_program = match WitnessProgram::new(version, program) { - Ok(witness_program) => witness_program, - Err(_) => return None, - }; - Some(Address::new(network, Payload::WitnessProgram(witness_program))) - }; - self.fields().fallbacks .as_ref() - .map(|fallbacks| fallbacks.iter().filter_map(to_valid_address).collect()) + .map(|fallbacks| filter_fallbacks(self.chain(), fallbacks)) .unwrap_or_else(Vec::new) } @@ -1075,6 +1041,50 @@ impl InvoiceContents { } } +#[cfg(feature = "std")] +pub(super) fn is_expired(created_at: Duration, relative_expiry: Duration) -> bool { + let absolute_expiry = created_at.checked_add(relative_expiry); + match absolute_expiry { + Some(seconds_from_epoch) => match SystemTime::UNIX_EPOCH.elapsed() { + Ok(elapsed) => elapsed > seconds_from_epoch, + Err(_) => false, + }, + None => false, + } +} + +pub(super) fn filter_fallbacks( + chain: ChainHash, fallbacks: &Vec +) -> Vec
{ + let network = if chain == ChainHash::using_genesis_block(Network::Bitcoin) { + Network::Bitcoin + } else if chain == ChainHash::using_genesis_block(Network::Testnet) { + Network::Testnet + } else if chain == ChainHash::using_genesis_block(Network::Signet) { + Network::Signet + } else if chain == ChainHash::using_genesis_block(Network::Regtest) { + Network::Regtest + } else { + return Vec::new() + }; + + let to_valid_address = |address: &FallbackAddress| { + let version = match WitnessVersion::try_from(address.version) { + Ok(version) => version, + Err(_) => return None, + }; + + let program = address.program.clone(); + let witness_program = match WitnessProgram::new(version, program) { + Ok(witness_program) => witness_program, + Err(_) => return None, + }; + Some(Address::new(network, Payload::WitnessProgram(witness_program))) + }; + + fallbacks.iter().filter_map(to_valid_address).collect() +} + impl InvoiceFields { fn as_tlv_stream(&self) -> InvoiceTlvStreamRef { let features = { @@ -1154,12 +1164,12 @@ tlv_stream!(InvoiceTlvStream, InvoiceTlvStreamRef, 160..240, { (176, node_id: PublicKey), }); -type BlindedPathIter<'a> = core::iter::Map< +pub(super) type BlindedPathIter<'a> = core::iter::Map< core::slice::Iter<'a, (BlindedPayInfo, BlindedPath)>, for<'r> fn(&'r (BlindedPayInfo, BlindedPath)) -> &'r BlindedPath, >; -type BlindedPayInfoIter<'a> = core::iter::Map< +pub(super) type BlindedPayInfoIter<'a> = core::iter::Map< core::slice::Iter<'a, (BlindedPayInfo, BlindedPath)>, for<'r> fn(&'r (BlindedPayInfo, BlindedPath)) -> &'r BlindedPayInfo, >; @@ -1205,8 +1215,8 @@ impl_writeable!(BlindedPayInfo, { /// Wire representation for an on-chain fallback address. #[derive(Clone, Debug, PartialEq)] pub(super) struct FallbackAddress { - version: u8, - program: Vec, + pub(super) version: u8, + pub(super) program: Vec, } impl_writeable!(FallbackAddress, { version, program }); @@ -1294,17 +1304,7 @@ impl TryFrom for InvoiceContents { }, ) = tlv_stream; - let payment_paths = match (blindedpay, paths) { - (_, None) => return Err(Bolt12SemanticError::MissingPaths), - (None, _) => return Err(Bolt12SemanticError::InvalidPayInfo), - (_, Some(paths)) if paths.is_empty() => return Err(Bolt12SemanticError::MissingPaths), - (Some(blindedpay), Some(paths)) if paths.len() != blindedpay.len() => { - return Err(Bolt12SemanticError::InvalidPayInfo); - }, - (Some(blindedpay), Some(paths)) => { - blindedpay.into_iter().zip(paths.into_iter()).collect::>() - }, - }; + let payment_paths = construct_payment_paths(blindedpay, paths)?; let created_at = match created_at { None => return Err(Bolt12SemanticError::MissingCreationTime), @@ -1372,6 +1372,22 @@ impl TryFrom for InvoiceContents { } } +pub(super) fn construct_payment_paths( + blinded_payinfos: Option>, blinded_paths: Option> +) -> Result, Bolt12SemanticError> { + match (blinded_payinfos, blinded_paths) { + (_, None) => Err(Bolt12SemanticError::MissingPaths), + (None, _) => Err(Bolt12SemanticError::InvalidPayInfo), + (_, Some(paths)) if paths.is_empty() => Err(Bolt12SemanticError::MissingPaths), + (Some(blindedpay), Some(paths)) if paths.len() != blindedpay.len() => { + Err(Bolt12SemanticError::InvalidPayInfo) + }, + (Some(blindedpay), Some(paths)) => { + Ok(blindedpay.into_iter().zip(paths.into_iter()).collect::>()) + }, + } +} + #[cfg(test)] mod tests { use super::{Bolt12Invoice, DEFAULT_RELATIVE_EXPIRY, FallbackAddress, FullInvoiceTlvStreamRef, InvoiceTlvStreamRef, SIGNATURE_TAG, UnsignedBolt12Invoice}; From f6bd1ebfc59564fc3546c5eb7c9762d607cb4217 Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Tue, 4 Jun 2024 18:21:50 -0400 Subject: [PATCH 06/12] BOLT 12 invoice: extract helper for invoice signing pubkey checks Will be useful for static invoices. --- lightning/src/offers/invoice.rs | 64 +++++++++++++++++---------------- 1 file changed, 34 insertions(+), 30 deletions(-) diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index 189033758..ac2b4aa76 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -1337,37 +1337,18 @@ impl TryFrom for InvoiceContents { features, signing_pubkey, }; - match (offer_tlv_stream.node_id, &offer_tlv_stream.paths) { - (Some(expected_signing_pubkey), _) => { - if fields.signing_pubkey != expected_signing_pubkey { - return Err(Bolt12SemanticError::InvalidSigningPubkey); - } + check_invoice_signing_pubkey(&fields.signing_pubkey, &offer_tlv_stream)?; - let invoice_request = InvoiceRequestContents::try_from( - (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream) - )?; - Ok(InvoiceContents::ForOffer { invoice_request, fields }) - }, - (None, Some(paths)) => { - if !paths - .iter() - .filter_map(|path| path.blinded_hops.last()) - .any(|last_hop| fields.signing_pubkey == last_hop.blinded_node_id) - { - return Err(Bolt12SemanticError::InvalidSigningPubkey); - } - - let invoice_request = InvoiceRequestContents::try_from( - (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream) - )?; - Ok(InvoiceContents::ForOffer { invoice_request, fields }) - }, - (None, None) => { - let refund = RefundContents::try_from( - (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream) - )?; - Ok(InvoiceContents::ForRefund { refund, fields }) - }, + if offer_tlv_stream.node_id.is_none() && offer_tlv_stream.paths.is_none() { + let refund = RefundContents::try_from( + (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream) + )?; + Ok(InvoiceContents::ForRefund { refund, fields }) + } else { + let invoice_request = InvoiceRequestContents::try_from( + (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream) + )?; + Ok(InvoiceContents::ForOffer { invoice_request, fields }) } } } @@ -1388,6 +1369,29 @@ pub(super) fn construct_payment_paths( } } +pub(super) fn check_invoice_signing_pubkey( + invoice_signing_pubkey: &PublicKey, offer_tlv_stream: &OfferTlvStream +) -> Result<(), Bolt12SemanticError> { + match (&offer_tlv_stream.node_id, &offer_tlv_stream.paths) { + (Some(expected_signing_pubkey), _) => { + if invoice_signing_pubkey != expected_signing_pubkey { + return Err(Bolt12SemanticError::InvalidSigningPubkey); + } + }, + (None, Some(paths)) => { + if !paths + .iter() + .filter_map(|path| path.blinded_hops.last()) + .any(|last_hop| invoice_signing_pubkey == &last_hop.blinded_node_id) + { + return Err(Bolt12SemanticError::InvalidSigningPubkey); + } + }, + _ => {}, + } + Ok(()) +} + #[cfg(test)] mod tests { use super::{Bolt12Invoice, DEFAULT_RELATIVE_EXPIRY, FallbackAddress, FullInvoiceTlvStreamRef, InvoiceTlvStreamRef, SIGNATURE_TAG, UnsignedBolt12Invoice}; From 922fd601d2ce98483dca6cdff0907b37d7271dea Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Tue, 4 Jun 2024 18:17:45 -0400 Subject: [PATCH 07/12] Use ? instead of matching in BOLT 12 invoice parsing. Minor cleanup to be more concise. --- lightning/src/offers/invoice.rs | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index ac2b4aa76..0aed817b2 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -1278,10 +1278,9 @@ impl TryFrom> for Bolt12Invoice { (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream) )?; - let signature = match signature { - None => return Err(Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::MissingSignature)), - Some(signature) => signature, - }; + let signature = signature.ok_or( + Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::MissingSignature) + )?; let tagged_hash = TaggedHash::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &bytes); let pubkey = contents.fields().signing_pubkey; merkle::verify_signature(&signature, &tagged_hash, pubkey)?; @@ -1315,22 +1314,13 @@ impl TryFrom for InvoiceContents { .map(Into::::into) .map(Duration::from_secs); - let payment_hash = match payment_hash { - None => return Err(Bolt12SemanticError::MissingPaymentHash), - Some(payment_hash) => payment_hash, - }; + let payment_hash = payment_hash.ok_or(Bolt12SemanticError::MissingPaymentHash)?; - let amount_msats = match amount { - None => return Err(Bolt12SemanticError::MissingAmount), - Some(amount) => amount, - }; + let amount_msats = amount.ok_or(Bolt12SemanticError::MissingAmount)?; let features = features.unwrap_or_else(Bolt12InvoiceFeatures::empty); - let signing_pubkey = match node_id { - None => return Err(Bolt12SemanticError::MissingSigningPubkey), - Some(node_id) => node_id, - }; + let signing_pubkey = node_id.ok_or(Bolt12SemanticError::MissingSigningPubkey)?; let fields = InvoiceFields { payment_paths, created_at, relative_expiry, payment_hash, amount_msats, fallbacks, From 3cfb24d265ecd9a708aed0bb9de665760e16e999 Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Thu, 6 Jun 2024 14:43:10 -0400 Subject: [PATCH 08/12] InvoiceTlvStream{Ref}: add message_paths field Will be used in static invoices. Also test that we'll fail to decode if these paths are included in single-use BOLT 12 invoices. --- lightning/src/offers/invoice.rs | 40 ++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index 0aed817b2..6695121b3 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -1102,6 +1102,7 @@ impl InvoiceFields { fallbacks: self.fallbacks.as_ref(), features, node_id: Some(&self.signing_pubkey), + message_paths: None, } } } @@ -1162,6 +1163,8 @@ tlv_stream!(InvoiceTlvStream, InvoiceTlvStreamRef, 160..240, { (172, fallbacks: (Vec, WithoutLength)), (174, features: (Bolt12InvoiceFeatures, WithoutLength)), (176, node_id: PublicKey), + // Only present in `StaticInvoice`s. + (238, message_paths: (Vec, WithoutLength)), }); pub(super) type BlindedPathIter<'a> = core::iter::Map< @@ -1299,10 +1302,12 @@ impl TryFrom for InvoiceContents { invoice_request_tlv_stream, InvoiceTlvStream { paths, blindedpay, created_at, relative_expiry, payment_hash, amount, fallbacks, - features, node_id, + features, node_id, message_paths, }, ) = tlv_stream; + if message_paths.is_some() { return Err(Bolt12SemanticError::UnexpectedPaths) } + let payment_paths = construct_payment_paths(blindedpay, paths)?; let created_at = match created_at { @@ -1568,6 +1573,7 @@ mod tests { fallbacks: None, features: None, node_id: Some(&recipient_pubkey()), + message_paths: None, }, SignatureTlvStreamRef { signature: Some(&invoice.signature()) }, ), @@ -1659,6 +1665,7 @@ mod tests { fallbacks: None, features: None, node_id: Some(&recipient_pubkey()), + message_paths: None, }, SignatureTlvStreamRef { signature: Some(&invoice.signature()) }, ), @@ -2429,4 +2436,35 @@ mod tests { Err(e) => assert_eq!(e, Bolt12ParseError::Decode(DecodeError::InvalidValue)), } } + + #[test] + fn fails_parsing_invoice_with_message_paths() { + let invoice = OfferBuilder::new(recipient_pubkey()) + .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(recipient_sign).unwrap(); + + let blinded_path = BlindedPath { + introduction_node: IntroductionNode::NodeId(pubkey(40)), + blinding_point: pubkey(41), + blinded_hops: vec![ + BlindedHop { blinded_node_id: pubkey(42), encrypted_payload: vec![0; 43] }, + BlindedHop { blinded_node_id: pubkey(43), encrypted_payload: vec![0; 44] }, + ], + }; + + let mut tlv_stream = invoice.as_tlv_stream(); + let message_paths = vec![blinded_path]; + tlv_stream.3.message_paths = Some(&message_paths); + + match Bolt12Invoice::try_from(tlv_stream.to_bytes()) { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::UnexpectedPaths)), + } + } } From e3dea2c3c7099c63d569b8912b9633aa733be19a Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Thu, 9 May 2024 14:29:08 -0400 Subject: [PATCH 09/12] Static invoice encoding and parsing Define an interface for BOLT 12 static invoice messages. The underlying format consists of the original bytes and the parsed contents. The bytes are later needed for serialization. This is because it must mirror all the offer TLV records, including unknown ones, which aren't represented in the contents. Invoices may be created from an offer. --- lightning/src/offers/mod.rs | 2 + lightning/src/offers/parse.rs | 2 + lightning/src/offers/static_invoice.rs | 352 +++++++++++++++++++++++++ 3 files changed, 356 insertions(+) create mode 100644 lightning/src/offers/static_invoice.rs diff --git a/lightning/src/offers/mod.rs b/lightning/src/offers/mod.rs index eb9b18e18..b77eec161 100644 --- a/lightning/src/offers/mod.rs +++ b/lightning/src/offers/mod.rs @@ -24,5 +24,7 @@ pub mod parse; mod payer; pub mod refund; pub(crate) mod signer; +#[allow(unused)] +pub(crate) mod static_invoice; #[cfg(test)] pub(crate) mod test_utils; diff --git a/lightning/src/offers/parse.rs b/lightning/src/offers/parse.rs index 472e44f62..c48d745a9 100644 --- a/lightning/src/offers/parse.rs +++ b/lightning/src/offers/parse.rs @@ -189,6 +189,8 @@ pub enum Bolt12SemanticError { MissingCreationTime, /// An invoice payment hash was expected but was missing. MissingPaymentHash, + /// An invoice payment hash was provided but was not expected. + UnexpectedPaymentHash, /// A signature was expected but was missing. MissingSignature, } diff --git a/lightning/src/offers/static_invoice.rs b/lightning/src/offers/static_invoice.rs new file mode 100644 index 000000000..0cf5e9210 --- /dev/null +++ b/lightning/src/offers/static_invoice.rs @@ -0,0 +1,352 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! Data structures and encoding for static BOLT 12 invoices. + +use crate::blinded_path::BlindedPath; +use crate::io; +use crate::ln::features::{Bolt12InvoiceFeatures, OfferFeatures}; +use crate::ln::msgs::DecodeError; +use crate::offers::invoice::{ + check_invoice_signing_pubkey, construct_payment_paths, filter_fallbacks, BlindedPathIter, + BlindedPayInfo, BlindedPayInfoIter, FallbackAddress, InvoiceTlvStream, InvoiceTlvStreamRef, +}; +use crate::offers::invoice_macros::invoice_accessors_common; +use crate::offers::merkle::{self, SignatureTlvStream, TaggedHash}; +use crate::offers::offer::{Amount, OfferContents, OfferTlvStream, Quantity}; +use crate::offers::parse::{Bolt12ParseError, Bolt12SemanticError, ParsedMessage}; +use crate::util::ser::{ + HighZeroBytesDroppedBigSize, Iterable, SeekReadable, WithoutLength, Writeable, Writer, +}; +use crate::util::string::PrintableString; +use bitcoin::address::Address; +use bitcoin::blockdata::constants::ChainHash; +use bitcoin::secp256k1::schnorr::Signature; +use bitcoin::secp256k1::PublicKey; +use core::time::Duration; + +#[cfg(feature = "std")] +use crate::offers::invoice::is_expired; + +#[allow(unused_imports)] +use crate::prelude::*; + +/// Static invoices default to expiring after 2 weeks. +const DEFAULT_RELATIVE_EXPIRY: Duration = Duration::from_secs(3600 * 24 * 14); + +/// Tag for the hash function used when signing a [`StaticInvoice`]'s merkle root. +pub const SIGNATURE_TAG: &'static str = concat!("lightning", "static_invoice", "signature"); + +/// A `StaticInvoice` is a reusable payment request corresponding to an [`Offer`]. +/// +/// A static invoice may be sent in response to an [`InvoiceRequest`] and includes all the +/// information needed to pay the recipient. However, unlike [`Bolt12Invoice`]s, static invoices do +/// not provide proof-of-payment. Therefore, [`Bolt12Invoice`]s should be preferred when the +/// recipient is online to provide one. +/// +/// [`Offer`]: crate::offers::offer::Offer +/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest +/// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice +#[derive(Clone, Debug)] +pub struct StaticInvoice { + bytes: Vec, + contents: InvoiceContents, + signature: Signature, +} + +/// The contents of a [`StaticInvoice`] for responding to an [`Offer`]. +/// +/// [`Offer`]: crate::offers::offer::Offer +#[derive(Clone, Debug)] +struct InvoiceContents { + offer: OfferContents, + payment_paths: Vec<(BlindedPayInfo, BlindedPath)>, + created_at: Duration, + relative_expiry: Option, + fallbacks: Option>, + features: Bolt12InvoiceFeatures, + signing_pubkey: PublicKey, + message_paths: Vec, +} + +macro_rules! invoice_accessors { ($self: ident, $contents: expr) => { + /// The chain that must be used when paying the invoice. [`StaticInvoice`]s currently can only be + /// created from offers that support a single chain. + pub fn chain(&$self) -> ChainHash { + $contents.chain() + } + + /// Opaque bytes set by the originating [`Offer::metadata`]. + /// + /// [`Offer::metadata`]: crate::offers::offer::Offer::metadata + pub fn metadata(&$self) -> Option<&Vec> { + $contents.metadata() + } + + /// The minimum amount required for a successful payment of a single item. + /// + /// From [`Offer::amount`]. + /// + /// [`Offer::amount`]: crate::offers::offer::Offer::amount + pub fn amount(&$self) -> Option { + $contents.amount() + } + + /// Features pertaining to the originating [`Offer`], from [`Offer::offer_features`]. + /// + /// [`Offer`]: crate::offers::offer::Offer + /// [`Offer::offer_features`]: crate::offers::offer::Offer::offer_features + pub fn offer_features(&$self) -> &OfferFeatures { + $contents.offer_features() + } + + /// A complete description of the purpose of the originating offer, from [`Offer::description`]. + /// + /// [`Offer::description`]: crate::offers::offer::Offer::description + pub fn description(&$self) -> Option { + $contents.description() + } + + /// Duration since the Unix epoch when an invoice should no longer be requested, from + /// [`Offer::absolute_expiry`]. + /// + /// [`Offer::absolute_expiry`]: crate::offers::offer::Offer::absolute_expiry + pub fn absolute_expiry(&$self) -> Option { + $contents.absolute_expiry() + } + + /// The issuer of the offer, from [`Offer::issuer`]. + /// + /// [`Offer::issuer`]: crate::offers::offer::Offer::issuer + pub fn issuer(&$self) -> Option { + $contents.issuer() + } + + /// Paths to the node that may supply the invoice on the recipient's behalf, originating from + /// publicly reachable nodes. Taken from [`Offer::paths`]. + /// + /// [`Offer::paths`]: crate::offers::offer::Offer::paths + pub fn offer_message_paths(&$self) -> &[BlindedPath] { + $contents.offer_message_paths() + } + + /// Paths to the recipient for indicating that a held HTLC is available to claim when they next + /// come online. + pub fn message_paths(&$self) -> &[BlindedPath] { + $contents.message_paths() + } + + /// The quantity of items supported, from [`Offer::supported_quantity`]. + /// + /// [`Offer::supported_quantity`]: crate::offers::offer::Offer::supported_quantity + pub fn supported_quantity(&$self) -> Quantity { + $contents.supported_quantity() + } +} } + +impl StaticInvoice { + invoice_accessors_common!(self, self.contents, StaticInvoice); + invoice_accessors!(self, self.contents); + + /// Signature of the invoice verified using [`StaticInvoice::signing_pubkey`]. + pub fn signature(&self) -> Signature { + self.signature + } +} + +impl InvoiceContents { + fn chain(&self) -> ChainHash { + debug_assert_eq!(self.offer.chains().len(), 1); + self.offer.chains().first().cloned().unwrap_or_else(|| self.offer.implied_chain()) + } + + fn metadata(&self) -> Option<&Vec> { + self.offer.metadata() + } + + fn amount(&self) -> Option { + self.offer.amount() + } + + fn offer_features(&self) -> &OfferFeatures { + self.offer.features() + } + + fn description(&self) -> Option { + self.offer.description() + } + + fn absolute_expiry(&self) -> Option { + self.offer.absolute_expiry() + } + + fn issuer(&self) -> Option { + self.offer.issuer() + } + + fn offer_message_paths(&self) -> &[BlindedPath] { + self.offer.paths() + } + + fn message_paths(&self) -> &[BlindedPath] { + &self.message_paths[..] + } + + fn supported_quantity(&self) -> Quantity { + self.offer.supported_quantity() + } + + fn payment_paths(&self) -> &[(BlindedPayInfo, BlindedPath)] { + &self.payment_paths[..] + } + + fn created_at(&self) -> Duration { + self.created_at + } + + fn relative_expiry(&self) -> Duration { + self.relative_expiry.unwrap_or(DEFAULT_RELATIVE_EXPIRY) + } + + #[cfg(feature = "std")] + fn is_expired(&self) -> bool { + is_expired(self.created_at(), self.relative_expiry()) + } + + fn fallbacks(&self) -> Vec
{ + let chain = self.chain(); + self.fallbacks + .as_ref() + .map(|fallbacks| filter_fallbacks(chain, fallbacks)) + .unwrap_or_else(Vec::new) + } + + fn features(&self) -> &Bolt12InvoiceFeatures { + &self.features + } + + fn signing_pubkey(&self) -> PublicKey { + self.signing_pubkey + } +} + +impl Writeable for StaticInvoice { + fn write(&self, writer: &mut W) -> Result<(), io::Error> { + WithoutLength(&self.bytes).write(writer) + } +} + +impl TryFrom> for StaticInvoice { + type Error = Bolt12ParseError; + + fn try_from(bytes: Vec) -> Result { + let parsed_invoice = ParsedMessage::::try_from(bytes)?; + StaticInvoice::try_from(parsed_invoice) + } +} + +type FullInvoiceTlvStream = (OfferTlvStream, InvoiceTlvStream, SignatureTlvStream); + +impl SeekReadable for FullInvoiceTlvStream { + fn read(r: &mut R) -> Result { + let offer = SeekReadable::read(r)?; + let invoice = SeekReadable::read(r)?; + let signature = SeekReadable::read(r)?; + + Ok((offer, invoice, signature)) + } +} + +type PartialInvoiceTlvStream = (OfferTlvStream, InvoiceTlvStream); + +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 signature = match signature { + None => { + return Err(Bolt12ParseError::InvalidSemantics( + Bolt12SemanticError::MissingSignature, + )) + }, + Some(signature) => signature, + }; + let tagged_hash = TaggedHash::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &bytes); + let pubkey = contents.signing_pubkey; + merkle::verify_signature(&signature, &tagged_hash, pubkey)?; + + Ok(StaticInvoice { bytes, contents, signature }) + } +} + +impl TryFrom for InvoiceContents { + type Error = Bolt12SemanticError; + + fn try_from(tlv_stream: PartialInvoiceTlvStream) -> Result { + let ( + offer_tlv_stream, + InvoiceTlvStream { + paths, + blindedpay, + created_at, + relative_expiry, + fallbacks, + features, + node_id, + message_paths, + payment_hash, + amount, + }, + ) = tlv_stream; + + if payment_hash.is_some() { + return Err(Bolt12SemanticError::UnexpectedPaymentHash); + } + if amount.is_some() { + return Err(Bolt12SemanticError::UnexpectedAmount); + } + + let payment_paths = construct_payment_paths(blindedpay, paths)?; + let message_paths = message_paths.ok_or(Bolt12SemanticError::MissingPaths)?; + + let created_at = match created_at { + None => return Err(Bolt12SemanticError::MissingCreationTime), + Some(timestamp) => Duration::from_secs(timestamp), + }; + + let relative_expiry = relative_expiry.map(Into::::into).map(Duration::from_secs); + + let features = features.unwrap_or_else(Bolt12InvoiceFeatures::empty); + + let signing_pubkey = node_id.ok_or(Bolt12SemanticError::MissingSigningPubkey)?; + check_invoice_signing_pubkey(&signing_pubkey, &offer_tlv_stream)?; + + if offer_tlv_stream.paths.is_none() { + return Err(Bolt12SemanticError::MissingPaths); + } + if offer_tlv_stream.chains.as_ref().map_or(0, |chains| chains.len()) > 1 { + return Err(Bolt12SemanticError::UnexpectedChain); + } + + Ok(InvoiceContents { + offer: OfferContents::try_from(offer_tlv_stream)?, + payment_paths, + message_paths, + created_at, + relative_expiry, + fallbacks, + features, + signing_pubkey, + }) + } +} From 1e580668684d4dbf11d69d75e5d4a5c4f8cc40bf Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Thu, 9 May 2024 17:15:34 -0400 Subject: [PATCH 10/12] Builder for creating static invoices from offers Add a builder for creating static invoices for an offer. Building produces a semantically valid static invoice for the offer, which can then be signed with the key associated with the offer's signing pubkey. --- lightning/src/offers/offer.rs | 6 + lightning/src/offers/static_invoice.rs | 215 ++++++++++++++++++++++++- 2 files changed, 217 insertions(+), 4 deletions(-) diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs index db910b5e1..dd58c75ce 100644 --- a/lightning/src/offers/offer.rs +++ b/lightning/src/offers/offer.rs @@ -664,6 +664,12 @@ impl Offer { pub fn expects_quantity(&self) -> bool { self.contents.expects_quantity() } + + pub(super) fn verify( + &self, key: &ExpandedKey, secp_ctx: &Secp256k1 + ) -> Result<(OfferId, Option), ()> { + self.contents.verify(&self.bytes, key, secp_ctx) + } } macro_rules! request_invoice_derived_payer_id { ($self: ident, $builder: ty) => { diff --git a/lightning/src/offers/static_invoice.rs b/lightning/src/offers/static_invoice.rs index 0cf5e9210..e46e1bfd8 100644 --- a/lightning/src/offers/static_invoice.rs +++ b/lightning/src/offers/static_invoice.rs @@ -12,14 +12,19 @@ use crate::blinded_path::BlindedPath; use crate::io; 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, BlindedPathIter, BlindedPayInfo, BlindedPayInfoIter, FallbackAddress, InvoiceTlvStream, InvoiceTlvStreamRef, }; -use crate::offers::invoice_macros::invoice_accessors_common; -use crate::offers::merkle::{self, SignatureTlvStream, TaggedHash}; -use crate::offers::offer::{Amount, OfferContents, OfferTlvStream, Quantity}; +use crate::offers::invoice_macros::{invoice_accessors_common, invoice_builder_methods_common}; +use crate::offers::merkle::{ + self, SignError, SignFn, SignatureTlvStream, SignatureTlvStreamRef, TaggedHash, +}; +use crate::offers::offer::{ + Amount, Offer, OfferContents, OfferTlvStream, OfferTlvStreamRef, Quantity, +}; use crate::offers::parse::{Bolt12ParseError, Bolt12SemanticError, ParsedMessage}; use crate::util::ser::{ HighZeroBytesDroppedBigSize, Iterable, SeekReadable, WithoutLength, Writeable, Writer, @@ -28,7 +33,7 @@ use crate::util::string::PrintableString; use bitcoin::address::Address; use bitcoin::blockdata::constants::ChainHash; use bitcoin::secp256k1::schnorr::Signature; -use bitcoin::secp256k1::PublicKey; +use bitcoin::secp256k1::{self, Keypair, PublicKey, Secp256k1}; use core::time::Duration; #[cfg(feature = "std")] @@ -75,6 +80,93 @@ struct InvoiceContents { message_paths: Vec, } +/// Builds a [`StaticInvoice`] from an [`Offer`]. +/// +/// [`Offer`]: crate::offers::offer::Offer +/// This is not exported to bindings users as builder patterns don't map outside of move semantics. +// TODO: add module-level docs and link here +pub struct StaticInvoiceBuilder<'a> { + offer_bytes: &'a Vec, + invoice: InvoiceContents, + keys: Keypair, +} + +impl<'a> StaticInvoiceBuilder<'a> { + /// Initialize a [`StaticInvoiceBuilder`] from the given [`Offer`]. + /// + /// Unless [`StaticInvoiceBuilder::relative_expiry`] is set, the invoice will expire 24 hours + /// after `created_at`. + pub fn for_offer_using_derived_keys( + offer: &'a Offer, payment_paths: Vec<(BlindedPayInfo, BlindedPath)>, + message_paths: Vec, created_at: Duration, expanded_key: &ExpandedKey, + secp_ctx: &Secp256k1, + ) -> Result { + if offer.chains().len() > 1 { + return Err(Bolt12SemanticError::UnexpectedChain); + } + + if payment_paths.is_empty() || message_paths.is_empty() || offer.paths().is_empty() { + return Err(Bolt12SemanticError::MissingPaths); + } + + let offer_signing_pubkey = + offer.signing_pubkey().ok_or(Bolt12SemanticError::MissingSigningPubkey)?; + + let keys = offer + .verify(&expanded_key, &secp_ctx) + .map_err(|()| Bolt12SemanticError::InvalidMetadata)? + .1 + .ok_or(Bolt12SemanticError::MissingSigningPubkey)?; + + let signing_pubkey = keys.public_key(); + if signing_pubkey != offer_signing_pubkey { + return Err(Bolt12SemanticError::InvalidSigningPubkey); + } + + let invoice = + InvoiceContents::new(offer, payment_paths, message_paths, created_at, signing_pubkey); + + 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 { + #[cfg(feature = "std")] + { + if self.invoice.is_offer_expired() { + return Err(Bolt12SemanticError::AlreadyExpired); + } + } + + #[cfg(not(feature = "std"))] + { + if self.invoice.is_offer_expired_no_std(self.invoice.created_at()) { + return Err(Bolt12SemanticError::AlreadyExpired); + } + } + + let Self { offer_bytes, invoice, keys } = self; + let unsigned_invoice = UnsignedStaticInvoice::new(&offer_bytes, invoice); + let invoice = unsigned_invoice + .sign(|message: &UnsignedStaticInvoice| { + Ok(secp_ctx.sign_schnorr_no_aux_rand(message.tagged_hash.as_digest(), &keys)) + }) + .unwrap(); + Ok(invoice) + } + + invoice_builder_methods_common!(self, Self, self.invoice, Self, self, S, StaticInvoice, mut); +} + +/// A semantically valid [`StaticInvoice`] that hasn't been signed. +pub struct UnsignedStaticInvoice { + bytes: Vec, + contents: InvoiceContents, + tagged_hash: TaggedHash, +} + macro_rules! invoice_accessors { ($self: ident, $contents: expr) => { /// The chain that must be used when paying the invoice. [`StaticInvoice`]s currently can only be /// created from offers that support a single chain. @@ -150,6 +242,68 @@ macro_rules! invoice_accessors { ($self: ident, $contents: expr) => { } } } +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(); + + let tagged_hash = TaggedHash::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &bytes); + + Self { contents, tagged_hash, bytes } + } + + /// Signs the [`TaggedHash`] of the invoice using the given function. + /// + /// Note: The hash computation may have included unknown, odd TLV records. + pub fn sign(mut self, sign: F) -> Result { + let pubkey = self.contents.signing_pubkey; + let signature = merkle::sign_message(sign, &self, pubkey)?; + + // Append the signature TLV record to the bytes. + let signature_tlv_stream = SignatureTlvStreamRef { signature: Some(&signature) }; + signature_tlv_stream.write(&mut self.bytes).unwrap(); + + Ok(StaticInvoice { bytes: self.bytes, contents: self.contents, signature }) + } + + invoice_accessors_common!(self, self.contents, StaticInvoice); + invoice_accessors!(self, self.contents); +} + +impl AsRef for UnsignedStaticInvoice { + fn as_ref(&self) -> &TaggedHash { + &self.tagged_hash + } +} + +/// A function for signing an [`UnsignedStaticInvoice`]. +pub trait SignStaticInvoiceFn { + /// Signs a [`TaggedHash`] computed over the merkle root of `message`'s TLV stream. + fn sign_invoice(&self, message: &UnsignedStaticInvoice) -> Result; +} + +impl SignStaticInvoiceFn for F +where + F: Fn(&UnsignedStaticInvoice) -> Result, +{ + fn sign_invoice(&self, message: &UnsignedStaticInvoice) -> Result { + self(message) + } +} + +impl SignFn for F +where + F: SignStaticInvoiceFn, +{ + fn sign(&self, message: &UnsignedStaticInvoice) -> Result { + self.sign_invoice(message) + } +} + impl StaticInvoice { invoice_accessors_common!(self, self.contents, StaticInvoice); invoice_accessors!(self, self.contents); @@ -161,6 +315,57 @@ impl StaticInvoice { } impl InvoiceContents { + #[cfg(feature = "std")] + fn is_offer_expired(&self) -> bool { + self.offer.is_expired() + } + + #[cfg(not(feature = "std"))] + fn is_offer_expired_no_std(&self, duration_since_epoch: Duration) -> bool { + self.offer.is_expired_no_std(duration_since_epoch) + } + + fn new( + offer: &Offer, payment_paths: Vec<(BlindedPayInfo, BlindedPath)>, + message_paths: Vec, created_at: Duration, signing_pubkey: PublicKey, + ) -> Self { + Self { + offer: offer.contents.clone(), + payment_paths, + message_paths, + created_at, + relative_expiry: None, + fallbacks: None, + features: Bolt12InvoiceFeatures::empty(), + signing_pubkey, + } + } + + fn as_tlv_stream(&self) -> PartialInvoiceTlvStreamRef { + let features = { + if self.features == Bolt12InvoiceFeatures::empty() { + None + } else { + Some(&self.features) + } + }; + + let invoice = InvoiceTlvStreamRef { + paths: Some(Iterable(self.payment_paths.iter().map(|(_, path)| path))), + message_paths: Some(self.message_paths.as_ref()), + blindedpay: Some(Iterable(self.payment_paths.iter().map(|(payinfo, _)| payinfo))), + created_at: Some(self.created_at.as_secs()), + relative_expiry: self.relative_expiry.map(|duration| duration.as_secs() as u32), + fallbacks: self.fallbacks.as_ref(), + features, + node_id: Some(&self.signing_pubkey), + amount: None, + payment_hash: None, + }; + + (self.offer.as_tlv_stream(), invoice) + } + fn chain(&self) -> ChainHash { debug_assert_eq!(self.offer.chains().len(), 1); self.offer.chains().first().cloned().unwrap_or_else(|| self.offer.implied_chain()) @@ -265,6 +470,8 @@ impl SeekReadable for FullInvoiceTlvStream { type PartialInvoiceTlvStream = (OfferTlvStream, InvoiceTlvStream); +type PartialInvoiceTlvStreamRef<'a> = (OfferTlvStreamRef<'a>, InvoiceTlvStreamRef<'a>); + impl TryFrom> for StaticInvoice { type Error = Bolt12ParseError; From 7970de47a6a0bf12cd28b9f5e075e289b5543394 Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Tue, 4 Jun 2024 19:12:54 -0400 Subject: [PATCH 11/12] Static invoice building tests --- lightning/src/offers/static_invoice.rs | 347 +++++++++++++++++++++++++ 1 file changed, 347 insertions(+) diff --git a/lightning/src/offers/static_invoice.rs b/lightning/src/offers/static_invoice.rs index e46e1bfd8..abfa2c1ea 100644 --- a/lightning/src/offers/static_invoice.rs +++ b/lightning/src/offers/static_invoice.rs @@ -557,3 +557,350 @@ impl TryFrom for InvoiceContents { }) } } + +#[cfg(test)] +mod tests { + use crate::blinded_path::{BlindedHop, BlindedPath, IntroductionNode}; + use crate::ln::features::{Bolt12InvoiceFeatures, OfferFeatures}; + use crate::ln::inbound_payment::ExpandedKey; + use crate::offers::invoice::InvoiceTlvStreamRef; + use crate::offers::merkle; + use crate::offers::merkle::{SignatureTlvStreamRef, TaggedHash}; + use crate::offers::offer::{Offer, OfferBuilder, OfferTlvStreamRef, Quantity}; + use crate::offers::parse::Bolt12SemanticError; + use crate::offers::static_invoice::{ + StaticInvoice, StaticInvoiceBuilder, DEFAULT_RELATIVE_EXPIRY, SIGNATURE_TAG, + }; + use crate::offers::test_utils::*; + use crate::sign::KeyMaterial; + use crate::util::ser::{Iterable, Writeable}; + use bitcoin::blockdata::constants::ChainHash; + use bitcoin::secp256k1::Secp256k1; + use bitcoin::Network; + use core::time::Duration; + + type FullInvoiceTlvStreamRef<'a> = + (OfferTlvStreamRef<'a>, InvoiceTlvStreamRef<'a>, SignatureTlvStreamRef<'a>); + + impl StaticInvoice { + fn as_tlv_stream(&self) -> FullInvoiceTlvStreamRef { + let (offer_tlv_stream, invoice_tlv_stream) = self.contents.as_tlv_stream(); + ( + offer_tlv_stream, + invoice_tlv_stream, + SignatureTlvStreamRef { signature: Some(&self.signature) }, + ) + } + } + + fn blinded_path() -> BlindedPath { + BlindedPath { + introduction_node: IntroductionNode::NodeId(pubkey(40)), + blinding_point: pubkey(41), + blinded_hops: vec![ + BlindedHop { blinded_node_id: pubkey(42), encrypted_payload: vec![0; 43] }, + BlindedHop { blinded_node_id: pubkey(43), encrypted_payload: vec![0; 44] }, + ], + } + } + + #[test] + fn builds_invoice_for_offer_with_defaults() { + 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 secp_ctx = Secp256k1::new(); + + let offer = + OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, &entropy, &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, + &secp_ctx, + ) + .unwrap() + .build_and_sign(&secp_ctx) + .unwrap(); + + let mut buffer = Vec::new(); + invoice.write(&mut buffer).unwrap(); + + assert_eq!(invoice.bytes, buffer.as_slice()); + assert!(invoice.metadata().is_some()); + assert_eq!(invoice.amount(), None); + assert_eq!(invoice.description(), None); + assert_eq!(invoice.offer_features(), &OfferFeatures::empty()); + assert_eq!(invoice.absolute_expiry(), None); + assert_eq!(invoice.offer_message_paths(), &[blinded_path()]); + assert_eq!(invoice.message_paths(), &[blinded_path()]); + assert_eq!(invoice.issuer(), None); + assert_eq!(invoice.supported_quantity(), Quantity::One); + assert_ne!(invoice.signing_pubkey(), recipient_pubkey()); + assert_eq!(invoice.chain(), ChainHash::using_genesis_block(Network::Bitcoin)); + assert_eq!(invoice.payment_paths(), payment_paths.as_slice()); + assert_eq!(invoice.created_at(), now); + assert_eq!(invoice.relative_expiry(), DEFAULT_RELATIVE_EXPIRY); + #[cfg(feature = "std")] + assert!(!invoice.is_expired()); + assert!(invoice.fallbacks().is_empty()); + assert_eq!(invoice.invoice_features(), &Bolt12InvoiceFeatures::empty()); + + let offer_signing_pubkey = offer.signing_pubkey().unwrap(); + let message = TaggedHash::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &invoice.bytes); + assert!( + merkle::verify_signature(&invoice.signature, &message, offer_signing_pubkey).is_ok() + ); + + let paths = vec![blinded_path()]; + let metadata = vec![42; 16]; + assert_eq!( + invoice.as_tlv_stream(), + ( + OfferTlvStreamRef { + chains: None, + metadata: Some(&metadata), + currency: None, + amount: None, + description: None, + features: None, + absolute_expiry: None, + paths: Some(&paths), + issuer: None, + quantity_max: None, + node_id: Some(&offer_signing_pubkey), + }, + InvoiceTlvStreamRef { + paths: Some(Iterable(payment_paths.iter().map(|(_, path)| path))), + blindedpay: Some(Iterable(payment_paths.iter().map(|(payinfo, _)| payinfo))), + created_at: Some(now.as_secs()), + relative_expiry: None, + payment_hash: None, + amount: None, + fallbacks: None, + features: None, + node_id: Some(&offer_signing_pubkey), + message_paths: Some(&paths), + }, + SignatureTlvStreamRef { signature: Some(&invoice.signature()) }, + ) + ); + + if let Err(e) = StaticInvoice::try_from(buffer) { + panic!("error parsing invoice: {:?}", e); + } + } + + #[cfg(feature = "std")] + #[test] + fn builds_invoice_from_offer_with_expiration() { + let node_id = recipient_pubkey(); + let now = now(); + let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32])); + let entropy = FixedEntropy {}; + let secp_ctx = Secp256k1::new(); + + let future_expiry = Duration::from_secs(u64::max_value()); + let past_expiry = Duration::from_secs(0); + + let valid_offer = + OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, &entropy, &secp_ctx) + .path(blinded_path()) + .absolute_expiry(future_expiry) + .build() + .unwrap(); + + let invoice = StaticInvoiceBuilder::for_offer_using_derived_keys( + &valid_offer, + payment_paths(), + vec![blinded_path()], + now, + &expanded_key, + &secp_ctx, + ) + .unwrap() + .build_and_sign(&secp_ctx) + .unwrap(); + assert!(!invoice.is_expired()); + assert_eq!(invoice.absolute_expiry(), Some(future_expiry)); + + let expired_offer = + OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, &entropy, &secp_ctx) + .path(blinded_path()) + .absolute_expiry(past_expiry) + .build() + .unwrap(); + if let Err(e) = StaticInvoiceBuilder::for_offer_using_derived_keys( + &expired_offer, + payment_paths(), + vec![blinded_path()], + now, + &expanded_key, + &secp_ctx, + ) + .unwrap() + .build_and_sign(&secp_ctx) + { + assert_eq!(e, Bolt12SemanticError::AlreadyExpired); + } else { + panic!("expected error") + } + } + + #[test] + fn fails_build_with_missing_paths() { + let node_id = recipient_pubkey(); + let now = now(); + let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32])); + let entropy = FixedEntropy {}; + let secp_ctx = Secp256k1::new(); + + let valid_offer = + OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, &entropy, &secp_ctx) + .path(blinded_path()) + .build() + .unwrap(); + + // Error if payment paths are missing. + if let Err(e) = StaticInvoiceBuilder::for_offer_using_derived_keys( + &valid_offer, + Vec::new(), + vec![blinded_path()], + now, + &expanded_key, + &secp_ctx, + ) { + assert_eq!(e, Bolt12SemanticError::MissingPaths); + } else { + panic!("expected error") + } + + // Error if message paths are missing. + if let Err(e) = StaticInvoiceBuilder::for_offer_using_derived_keys( + &valid_offer, + payment_paths(), + Vec::new(), + now, + &expanded_key, + &secp_ctx, + ) { + assert_eq!(e, Bolt12SemanticError::MissingPaths); + } else { + panic!("expected error") + } + + // 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(); + offer_tlv_stream.paths.take(); + let mut buffer = Vec::new(); + offer_tlv_stream.write(&mut buffer).unwrap(); + offer_without_paths = Offer::try_from(buffer).unwrap(); + if let Err(e) = StaticInvoiceBuilder::for_offer_using_derived_keys( + &offer_without_paths, + payment_paths(), + vec![blinded_path()], + now, + &expanded_key, + &secp_ctx, + ) { + assert_eq!(e, Bolt12SemanticError::MissingPaths); + } else { + panic!("expected error") + } + } + + #[test] + fn fails_build_offer_signing_pubkey() { + let node_id = recipient_pubkey(); + let now = now(); + let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32])); + let entropy = FixedEntropy {}; + let secp_ctx = Secp256k1::new(); + + let valid_offer = + OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, &entropy, &secp_ctx) + .path(blinded_path()) + .build() + .unwrap(); + + // Error if offer signing pubkey is missing. + let mut offer_missing_signing_pubkey = valid_offer.clone(); + let mut offer_tlv_stream = offer_missing_signing_pubkey.as_tlv_stream(); + offer_tlv_stream.node_id.take(); + let mut buffer = Vec::new(); + offer_tlv_stream.write(&mut buffer).unwrap(); + offer_missing_signing_pubkey = Offer::try_from(buffer).unwrap(); + + if let Err(e) = StaticInvoiceBuilder::for_offer_using_derived_keys( + &offer_missing_signing_pubkey, + payment_paths(), + vec![blinded_path()], + now, + &expanded_key, + &secp_ctx, + ) { + assert_eq!(e, Bolt12SemanticError::MissingSigningPubkey); + } else { + panic!("expected error") + } + + // Error if the offer's metadata cannot be verified. + let offer = OfferBuilder::new(recipient_pubkey()) + .path(blinded_path()) + .metadata(vec![42; 32]) + .unwrap() + .build() + .unwrap(); + if let Err(e) = StaticInvoiceBuilder::for_offer_using_derived_keys( + &offer, + payment_paths(), + vec![blinded_path()], + now, + &expanded_key, + &secp_ctx, + ) { + assert_eq!(e, Bolt12SemanticError::InvalidMetadata); + } else { + panic!("expected error") + } + } + + #[test] + fn fails_building_with_extra_offer_chains() { + let node_id = recipient_pubkey(); + let now = now(); + let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32])); + let entropy = FixedEntropy {}; + let secp_ctx = Secp256k1::new(); + + let offer_with_extra_chain = + OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, &entropy, &secp_ctx) + .path(blinded_path()) + .chain(Network::Bitcoin) + .chain(Network::Testnet) + .build() + .unwrap(); + + if let Err(e) = StaticInvoiceBuilder::for_offer_using_derived_keys( + &offer_with_extra_chain, + payment_paths(), + vec![blinded_path()], + now, + &expanded_key, + &secp_ctx, + ) { + assert_eq!(e, Bolt12SemanticError::UnexpectedChain); + } else { + panic!("expected error") + } + } +} From bafe4ed218dd9229bf7ad435fdc9a74e0ffb3f53 Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Fri, 31 May 2024 16:17:38 -0400 Subject: [PATCH 12/12] Static invoice parsing tests --- lightning/src/offers/static_invoice.rs | 271 ++++++++++++++++++++++++- 1 file changed, 268 insertions(+), 3 deletions(-) diff --git a/lightning/src/offers/static_invoice.rs b/lightning/src/offers/static_invoice.rs index abfa2c1ea..d0846b29a 100644 --- a/lightning/src/offers/static_invoice.rs +++ b/lightning/src/offers/static_invoice.rs @@ -563,19 +563,20 @@ mod tests { use crate::blinded_path::{BlindedHop, BlindedPath, IntroductionNode}; 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::merkle; use crate::offers::merkle::{SignatureTlvStreamRef, TaggedHash}; use crate::offers::offer::{Offer, OfferBuilder, OfferTlvStreamRef, Quantity}; - use crate::offers::parse::Bolt12SemanticError; + use crate::offers::parse::{Bolt12ParseError, Bolt12SemanticError}; use crate::offers::static_invoice::{ StaticInvoice, StaticInvoiceBuilder, DEFAULT_RELATIVE_EXPIRY, SIGNATURE_TAG, }; use crate::offers::test_utils::*; use crate::sign::KeyMaterial; - use crate::util::ser::{Iterable, Writeable}; + use crate::util::ser::{BigSize, Iterable, Writeable}; use bitcoin::blockdata::constants::ChainHash; - use bitcoin::secp256k1::Secp256k1; + use bitcoin::secp256k1::{self, Secp256k1}; use bitcoin::Network; use core::time::Duration; @@ -593,6 +594,43 @@ mod tests { } } + fn tlv_stream_to_bytes( + tlv_stream: &(OfferTlvStreamRef, InvoiceTlvStreamRef, SignatureTlvStreamRef), + ) -> 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(); + buffer + } + + fn invoice() -> StaticInvoice { + 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 secp_ctx = Secp256k1::new(); + + let offer = + OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, &entropy, &secp_ctx) + .path(blinded_path()) + .build() + .unwrap(); + + StaticInvoiceBuilder::for_offer_using_derived_keys( + &offer, + payment_paths.clone(), + vec![blinded_path()], + now, + &expanded_key, + &secp_ctx, + ) + .unwrap() + .build_and_sign(&secp_ctx) + .unwrap() + } + fn blinded_path() -> BlindedPath { BlindedPath { introduction_node: IntroductionNode::NodeId(pubkey(40)), @@ -903,4 +941,231 @@ mod tests { panic!("expected error") } } + + #[test] + fn parses_invoice_with_relative_expiry() { + 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 secp_ctx = Secp256k1::new(); + + let offer = + OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, &entropy, &secp_ctx) + .path(blinded_path()) + .build() + .unwrap(); + + const TEST_RELATIVE_EXPIRY: u32 = 3600; + let invoice = StaticInvoiceBuilder::for_offer_using_derived_keys( + &offer, + payment_paths.clone(), + vec![blinded_path()], + now, + &expanded_key, + &secp_ctx, + ) + .unwrap() + .relative_expiry(TEST_RELATIVE_EXPIRY) + .build_and_sign(&secp_ctx) + .unwrap(); + + let mut buffer = Vec::new(); + invoice.write(&mut buffer).unwrap(); + + match StaticInvoice::try_from(buffer) { + Ok(invoice) => assert_eq!( + invoice.relative_expiry(), + Duration::from_secs(TEST_RELATIVE_EXPIRY as u64) + ), + Err(e) => panic!("error parsing invoice: {:?}", e), + } + } + + #[test] + fn parses_invoice_with_allow_mpp() { + 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 secp_ctx = Secp256k1::new(); + + let offer = + OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, &entropy, &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, + &secp_ctx, + ) + .unwrap() + .allow_mpp() + .build_and_sign(&secp_ctx) + .unwrap(); + + let mut buffer = Vec::new(); + invoice.write(&mut buffer).unwrap(); + + match StaticInvoice::try_from(buffer) { + Ok(invoice) => { + let mut features = Bolt12InvoiceFeatures::empty(); + features.set_basic_mpp_optional(); + assert_eq!(invoice.invoice_features(), &features); + }, + Err(e) => panic!("error parsing invoice: {:?}", e), + } + } + + #[test] + fn fails_parsing_missing_invoice_fields() { + // Error if `created_at` is missing. + let missing_created_at_invoice = invoice(); + let mut tlv_stream = missing_created_at_invoice.as_tlv_stream(); + tlv_stream.1.created_at = None; + match StaticInvoice::try_from(tlv_stream_to_bytes(&tlv_stream)) { + Ok(_) => panic!("expected error"), + Err(e) => { + assert_eq!( + e, + Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::MissingCreationTime) + ); + }, + } + + // Error if `node_id` is missing. + let missing_node_id_invoice = invoice(); + let mut tlv_stream = missing_node_id_invoice.as_tlv_stream(); + tlv_stream.1.node_id = None; + match StaticInvoice::try_from(tlv_stream_to_bytes(&tlv_stream)) { + Ok(_) => panic!("expected error"), + Err(e) => { + assert_eq!( + e, + Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::MissingSigningPubkey) + ); + }, + } + + // Error if message paths are missing. + let missing_message_paths_invoice = invoice(); + let mut tlv_stream = missing_message_paths_invoice.as_tlv_stream(); + tlv_stream.1.message_paths = None; + match StaticInvoice::try_from(tlv_stream_to_bytes(&tlv_stream)) { + Ok(_) => panic!("expected error"), + Err(e) => { + assert_eq!( + e, + Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::MissingPaths) + ); + }, + } + + // Error if signature is missing. + let invoice = invoice(); + let mut buffer = Vec::new(); + invoice.contents.as_tlv_stream().write(&mut buffer).unwrap(); + match StaticInvoice::try_from(buffer) { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!( + e, + Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::MissingSignature) + ), + } + } + + #[test] + fn fails_parsing_invalid_signing_pubkey() { + let invoice = invoice(); + let invalid_pubkey = payer_pubkey(); + let mut tlv_stream = invoice.as_tlv_stream(); + tlv_stream.1.node_id = Some(&invalid_pubkey); + + match StaticInvoice::try_from(tlv_stream_to_bytes(&tlv_stream)) { + Ok(_) => panic!("expected error"), + Err(e) => { + assert_eq!( + e, + Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::InvalidSigningPubkey) + ); + }, + } + } + + #[test] + fn fails_parsing_invoice_with_invalid_signature() { + let mut invoice = invoice(); + let last_signature_byte = invoice.bytes.last_mut().unwrap(); + *last_signature_byte = last_signature_byte.wrapping_add(1); + + let mut buffer = Vec::new(); + invoice.write(&mut buffer).unwrap(); + + match StaticInvoice::try_from(buffer) { + Ok(_) => panic!("expected error"), + Err(e) => { + assert_eq!( + e, + Bolt12ParseError::InvalidSignature(secp256k1::Error::InvalidSignature) + ); + }, + } + } + + #[test] + fn fails_parsing_invoice_with_extra_tlv_records() { + let invoice = invoice(); + let mut encoded_invoice = Vec::new(); + invoice.write(&mut encoded_invoice).unwrap(); + BigSize(1002).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::Decode(DecodeError::InvalidValue)), + } + } + + #[test] + fn fails_parsing_invoice_with_invalid_offer_fields() { + // Error if the offer is missing paths. + let missing_offer_paths_invoice = invoice(); + let mut tlv_stream = missing_offer_paths_invoice.as_tlv_stream(); + tlv_stream.0.paths = None; + match StaticInvoice::try_from(tlv_stream_to_bytes(&tlv_stream)) { + Ok(_) => panic!("expected error"), + Err(e) => { + assert_eq!( + e, + Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::MissingPaths) + ); + }, + } + + // Error if the offer has more than one chain. + let invalid_offer_chains_invoice = invoice(); + let mut tlv_stream = invalid_offer_chains_invoice.as_tlv_stream(); + let invalid_chains = vec![ + ChainHash::using_genesis_block(Network::Bitcoin), + ChainHash::using_genesis_block(Network::Testnet), + ]; + tlv_stream.0.chains = Some(&invalid_chains); + match StaticInvoice::try_from(tlv_stream_to_bytes(&tlv_stream)) { + Ok(_) => panic!("expected error"), + Err(e) => { + assert_eq!( + e, + Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::UnexpectedChain) + ); + }, + } + } }