From 520fecf8fcbe1d162e8a1075d316ca994b9d2fd6 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Tue, 23 Apr 2024 17:38:13 -0500 Subject: [PATCH 1/7] Optional OfferContents::signing_pubkey If an Offer contains a path, the blinded_node_id of the path's final hop can be used as the signing pubkey. Make Offer::signing_pubkey and OfferContents::signing_pubkey return an Option to support this. Upcoming commits will implement this behavior. --- lightning/src/ln/channelmanager.rs | 19 +++++++++------- lightning/src/ln/offers_tests.rs | 12 +++++----- lightning/src/offers/invoice.rs | 5 ++--- lightning/src/offers/invoice_request.rs | 18 +++++++++++---- lightning/src/offers/offer.rs | 30 ++++++++++++++----------- 5 files changed, 50 insertions(+), 34 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 130eab49384..d10c627916a 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -8765,14 +8765,7 @@ where .map_err(|_| Bolt12SemanticError::DuplicatePaymentId)?; let mut pending_offers_messages = self.pending_offers_messages.lock().unwrap(); - if offer.paths().is_empty() { - let message = new_pending_onion_message( - OffersMessage::InvoiceRequest(invoice_request), - Destination::Node(offer.signing_pubkey()), - Some(reply_path), - ); - pending_offers_messages.push(message); - } else { + if !offer.paths().is_empty() { // Send as many invoice requests as there are paths in the offer (with an upper bound). // Using only one path could result in a failure if the path no longer exists. But only // one invoice for a given payment id will be paid, even if more than one is received. @@ -8785,6 +8778,16 @@ where ); pending_offers_messages.push(message); } + } else if let Some(signing_pubkey) = offer.signing_pubkey() { + let message = new_pending_onion_message( + OffersMessage::InvoiceRequest(invoice_request), + Destination::Node(signing_pubkey), + Some(reply_path), + ); + pending_offers_messages.push(message); + } else { + debug_assert!(false); + return Err(Bolt12SemanticError::MissingSigningPubkey); } Ok(()) diff --git a/lightning/src/ln/offers_tests.rs b/lightning/src/ln/offers_tests.rs index 75a2e290f39..e89e113e8ea 100644 --- a/lightning/src/ln/offers_tests.rs +++ b/lightning/src/ln/offers_tests.rs @@ -271,7 +271,7 @@ fn prefers_non_tor_nodes_in_blinded_paths() { .create_offer_builder("coffee".to_string()).unwrap() .amount_msats(10_000_000) .build().unwrap(); - assert_ne!(offer.signing_pubkey(), bob_id); + assert_ne!(offer.signing_pubkey(), Some(bob_id)); assert!(!offer.paths().is_empty()); for path in offer.paths() { assert_ne!(path.introduction_node, IntroductionNode::NodeId(bob_id)); @@ -286,7 +286,7 @@ fn prefers_non_tor_nodes_in_blinded_paths() { .create_offer_builder("coffee".to_string()).unwrap() .amount_msats(10_000_000) .build().unwrap(); - assert_ne!(offer.signing_pubkey(), bob_id); + assert_ne!(offer.signing_pubkey(), Some(bob_id)); assert!(!offer.paths().is_empty()); for path in offer.paths() { assert_eq!(path.introduction_node, IntroductionNode::NodeId(bob_id)); @@ -336,7 +336,7 @@ fn prefers_more_connected_nodes_in_blinded_paths() { .create_offer_builder("coffee".to_string()).unwrap() .amount_msats(10_000_000) .build().unwrap(); - assert_ne!(offer.signing_pubkey(), bob_id); + assert_ne!(offer.signing_pubkey(), Some(bob_id)); assert!(!offer.paths().is_empty()); for path in offer.paths() { assert_eq!(path.introduction_node, IntroductionNode::NodeId(nodes[4].node.get_our_node_id())); @@ -386,7 +386,7 @@ fn creates_and_pays_for_offer_using_two_hop_blinded_path() { .unwrap() .amount_msats(10_000_000) .build().unwrap(); - assert_ne!(offer.signing_pubkey(), alice_id); + assert_ne!(offer.signing_pubkey(), Some(alice_id)); assert!(!offer.paths().is_empty()); for path in offer.paths() { assert_eq!(path.introduction_node, IntroductionNode::NodeId(bob_id)); @@ -547,7 +547,7 @@ fn creates_and_pays_for_offer_using_one_hop_blinded_path() { .create_offer_builder("coffee".to_string()).unwrap() .amount_msats(10_000_000) .build().unwrap(); - assert_ne!(offer.signing_pubkey(), alice_id); + assert_ne!(offer.signing_pubkey(), Some(alice_id)); assert!(!offer.paths().is_empty()); for path in offer.paths() { assert_eq!(path.introduction_node, IntroductionNode::NodeId(alice_id)); @@ -672,7 +672,7 @@ fn pays_for_offer_without_blinded_paths() { .clear_paths() .amount_msats(10_000_000) .build().unwrap(); - assert_eq!(offer.signing_pubkey(), alice_id); + assert_eq!(offer.signing_pubkey(), Some(alice_id)); assert!(offer.paths().is_empty()); let payment_id = PaymentId([1; 32]); diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index ee5e6deb408..ab9e7bf8e64 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -210,10 +210,9 @@ macro_rules! invoice_explicit_signing_pubkey_builder_methods { ($self: ident, $s #[cfg_attr(c_bindings, allow(dead_code))] pub(super) fn for_offer( invoice_request: &'a InvoiceRequest, payment_paths: Vec<(BlindedPayInfo, BlindedPath)>, - created_at: Duration, payment_hash: PaymentHash + created_at: Duration, payment_hash: PaymentHash, signing_pubkey: PublicKey ) -> Result { let amount_msats = Self::amount_msats(invoice_request)?; - let signing_pubkey = invoice_request.contents.inner.offer.signing_pubkey(); let contents = InvoiceContents::ForOffer { invoice_request: invoice_request.contents.clone(), fields: Self::fields( @@ -272,7 +271,7 @@ macro_rules! invoice_derived_signing_pubkey_builder_methods { ($self: ident, $se created_at: Duration, payment_hash: PaymentHash, keys: KeyPair ) -> Result { let amount_msats = Self::amount_msats(invoice_request)?; - let signing_pubkey = invoice_request.contents.inner.offer.signing_pubkey(); + let signing_pubkey = keys.public_key(); let contents = InvoiceContents::ForOffer { invoice_request: invoice_request.contents.clone(), fields: Self::fields( diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index 9157613fcd9..a858e5afb2c 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -747,7 +747,12 @@ macro_rules! invoice_request_respond_with_explicit_signing_pubkey_methods { ( return Err(Bolt12SemanticError::UnknownRequiredFeatures); } - <$builder>::for_offer(&$contents, payment_paths, created_at, payment_hash) + let signing_pubkey = match $contents.contents.inner.offer.signing_pubkey() { + Some(signing_pubkey) => signing_pubkey, + None => return Err(Bolt12SemanticError::MissingSigningPubkey), + }; + + <$builder>::for_offer(&$contents, payment_paths, created_at, payment_hash, signing_pubkey) } } } @@ -855,6 +860,11 @@ macro_rules! invoice_request_respond_with_derived_signing_pubkey_methods { ( Some(keys) => keys, }; + match $contents.contents.inner.offer.signing_pubkey() { + Some(signing_pubkey) => debug_assert_eq!(signing_pubkey, keys.public_key()), + None => return Err(Bolt12SemanticError::MissingSigningPubkey), + } + <$builder>::for_offer_using_keys( &$self.inner, payment_paths, created_at, payment_hash, keys ) @@ -1233,7 +1243,7 @@ mod tests { assert_eq!(unsigned_invoice_request.paths(), &[]); assert_eq!(unsigned_invoice_request.issuer(), None); assert_eq!(unsigned_invoice_request.supported_quantity(), Quantity::One); - assert_eq!(unsigned_invoice_request.signing_pubkey(), recipient_pubkey()); + assert_eq!(unsigned_invoice_request.signing_pubkey(), Some(recipient_pubkey())); assert_eq!(unsigned_invoice_request.chain(), ChainHash::using_genesis_block(Network::Bitcoin)); assert_eq!(unsigned_invoice_request.amount_msats(), None); assert_eq!(unsigned_invoice_request.invoice_request_features(), &InvoiceRequestFeatures::empty()); @@ -1265,7 +1275,7 @@ mod tests { assert_eq!(invoice_request.paths(), &[]); assert_eq!(invoice_request.issuer(), None); assert_eq!(invoice_request.supported_quantity(), Quantity::One); - assert_eq!(invoice_request.signing_pubkey(), recipient_pubkey()); + assert_eq!(invoice_request.signing_pubkey(), Some(recipient_pubkey())); assert_eq!(invoice_request.chain(), ChainHash::using_genesis_block(Network::Bitcoin)); assert_eq!(invoice_request.amount_msats(), None); assert_eq!(invoice_request.invoice_request_features(), &InvoiceRequestFeatures::empty()); @@ -2260,7 +2270,7 @@ mod tests { .amount_msats(1000) .supported_quantity(Quantity::Unbounded) .build().unwrap(); - assert_eq!(offer.signing_pubkey(), node_id); + assert_eq!(offer.signing_pubkey(), Some(node_id)); let invoice_request = offer.request_invoice(vec![1; 32], payer_pubkey()).unwrap() .chain(Network::Testnet).unwrap() diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs index 3dedc6cd35d..b7852a0fc4b 100644 --- a/lightning/src/offers/offer.rs +++ b/lightning/src/offers/offer.rs @@ -226,7 +226,7 @@ macro_rules! offer_explicit_metadata_builder_methods { ( offer: OfferContents { chains: None, metadata: None, amount: None, description, features: OfferFeatures::empty(), absolute_expiry: None, issuer: None, paths: None, - supported_quantity: Quantity::One, signing_pubkey, + supported_quantity: Quantity::One, signing_pubkey: Some(signing_pubkey), }, metadata_strategy: core::marker::PhantomData, secp_ctx: None, @@ -265,7 +265,7 @@ macro_rules! offer_derived_metadata_builder_methods { ($secp_context: ty) => { offer: OfferContents { chains: None, metadata: Some(metadata), amount: None, description, features: OfferFeatures::empty(), absolute_expiry: None, issuer: None, paths: None, - supported_quantity: Quantity::One, signing_pubkey: node_id, + supported_quantity: Quantity::One, signing_pubkey: Some(node_id), }, metadata_strategy: core::marker::PhantomData, secp_ctx: Some(secp_ctx), @@ -391,7 +391,7 @@ macro_rules! offer_builder_methods { ( let (derived_metadata, keys) = metadata.derive_from(tlv_stream, $self.secp_ctx); metadata = derived_metadata; if let Some(keys) = keys { - $self.offer.signing_pubkey = keys.public_key(); + $self.offer.signing_pubkey = Some(keys.public_key()); } } @@ -542,7 +542,7 @@ pub(super) struct OfferContents { issuer: Option, paths: Option>, supported_quantity: Quantity, - signing_pubkey: PublicKey, + signing_pubkey: Option, } macro_rules! offer_accessors { ($self: ident, $contents: expr) => { @@ -604,7 +604,7 @@ macro_rules! offer_accessors { ($self: ident, $contents: expr) => { } /// The public key used by the recipient to sign invoices. - pub fn signing_pubkey(&$self) -> bitcoin::secp256k1::PublicKey { + pub fn signing_pubkey(&$self) -> Option { $contents.signing_pubkey() } } } @@ -886,7 +886,7 @@ impl OfferContents { } } - pub(super) fn signing_pubkey(&self) -> PublicKey { + pub(super) fn signing_pubkey(&self) -> Option { self.signing_pubkey } @@ -905,8 +905,12 @@ impl OfferContents { _ => true, } }); + let signing_pubkey = match self.signing_pubkey() { + Some(signing_pubkey) => signing_pubkey, + None => return Err(()), + }; let keys = signer::verify_recipient_metadata( - metadata, key, IV_BYTES, self.signing_pubkey(), tlv_stream, secp_ctx + metadata, key, IV_BYTES, signing_pubkey, tlv_stream, secp_ctx )?; let offer_id = OfferId::from_valid_invreq_tlv_stream(bytes); @@ -941,7 +945,7 @@ impl OfferContents { paths: self.paths.as_ref(), issuer: self.issuer.as_ref(), quantity_max: self.supported_quantity.to_tlv_record(), - node_id: Some(&self.signing_pubkey), + node_id: self.signing_pubkey.as_ref(), } } } @@ -1091,7 +1095,7 @@ impl TryFrom for OfferContents { let signing_pubkey = match node_id { None => return Err(Bolt12SemanticError::MissingSigningPubkey), - Some(node_id) => node_id, + Some(node_id) => Some(node_id), }; Ok(OfferContents { @@ -1154,7 +1158,7 @@ mod tests { assert_eq!(offer.paths(), &[]); assert_eq!(offer.issuer(), None); assert_eq!(offer.supported_quantity(), Quantity::One); - assert_eq!(offer.signing_pubkey(), pubkey(42)); + assert_eq!(offer.signing_pubkey(), Some(pubkey(42))); assert_eq!( offer.as_tlv_stream(), @@ -1251,7 +1255,7 @@ mod tests { ::deriving_signing_pubkey(desc, node_id, &expanded_key, &entropy, &secp_ctx) .amount_msats(1000) .build().unwrap(); - assert_eq!(offer.signing_pubkey(), node_id); + assert_eq!(offer.signing_pubkey(), Some(node_id)); let invoice_request = offer.request_invoice(vec![1; 32], payer_pubkey()).unwrap() .build().unwrap() @@ -1313,7 +1317,7 @@ mod tests { .amount_msats(1000) .path(blinded_path) .build().unwrap(); - assert_ne!(offer.signing_pubkey(), node_id); + assert_ne!(offer.signing_pubkey(), Some(node_id)); let invoice_request = offer.request_invoice(vec![1; 32], payer_pubkey()).unwrap() .build().unwrap() @@ -1471,7 +1475,7 @@ mod tests { .unwrap(); let tlv_stream = offer.as_tlv_stream(); assert_eq!(offer.paths(), paths.as_slice()); - assert_eq!(offer.signing_pubkey(), pubkey(42)); + assert_eq!(offer.signing_pubkey(), Some(pubkey(42))); assert_ne!(pubkey(42), pubkey(44)); assert_eq!(tlv_stream.paths, Some(&paths)); assert_eq!(tlv_stream.node_id, Some(&pubkey(42))); From 94d5af663d74177079124f15b40b65e16fbb1b27 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Wed, 24 Apr 2024 09:03:56 -0500 Subject: [PATCH 2/7] Allow parsing Offer without signing_pubkey If an offer has at least one path, it may omit the signing pubkey and use the blinded node id of the last hop of a path to sign an invoice. Allow parsing such offers but not yet creating them. --- lightning/src/offers/offer.rs | 36 ++++++++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs index b7852a0fc4b..8e93a93f873 100644 --- a/lightning/src/offers/offer.rs +++ b/lightning/src/offers/offer.rs @@ -436,6 +436,12 @@ macro_rules! offer_builder_test_methods { ( $return_value } + #[cfg_attr(c_bindings, allow(dead_code))] + pub(crate) fn clear_signing_pubkey($($self_mut)* $self: $self_type) -> $return_type { + $self.offer.signing_pubkey = None; + $return_value + } + #[cfg_attr(c_bindings, allow(dead_code))] pub(super) fn build_unchecked($self: $self_type) -> Offer { $self.build_without_checks() @@ -1093,9 +1099,10 @@ impl TryFrom for OfferContents { Some(n) => Quantity::Bounded(NonZeroU64::new(n).unwrap()), }; - let signing_pubkey = match node_id { - None => return Err(Bolt12SemanticError::MissingSigningPubkey), - Some(node_id) => Some(node_id), + let (signing_pubkey, paths) = match (node_id, paths) { + (None, None) => return Err(Bolt12SemanticError::MissingSigningPubkey), + (_, Some(paths)) if paths.is_empty() => return Err(Bolt12SemanticError::MissingPaths), + (node_id, paths) => (node_id, paths), }; Ok(OfferContents { @@ -1662,12 +1669,31 @@ mod tests { panic!("error parsing offer: {:?}", e); } + let offer = OfferBuilder::new("foo".into(), pubkey(42)) + .path(BlindedPath { + introduction_node: IntroductionNode::NodeId(pubkey(40)), + blinding_point: pubkey(41), + blinded_hops: vec![ + BlindedHop { blinded_node_id: pubkey(43), encrypted_payload: vec![0; 43] }, + BlindedHop { blinded_node_id: pubkey(44), encrypted_payload: vec![0; 44] }, + ], + }) + .clear_signing_pubkey() + .build() + .unwrap(); + if let Err(e) = offer.to_string().parse::() { + panic!("error parsing offer: {:?}", e); + } + let mut builder = OfferBuilder::new("foo".into(), pubkey(42)); builder.offer.paths = Some(vec![]); let offer = builder.build().unwrap(); - if let Err(e) = offer.to_string().parse::() { - panic!("error parsing offer: {:?}", e); + match offer.to_string().parse::() { + Ok(_) => panic!("expected error"), + Err(e) => { + assert_eq!(e, Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::MissingPaths)); + }, } } From 61e4fcea8e19eba1f6b1299c8415031c3a031a94 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Tue, 23 Apr 2024 18:30:59 -0500 Subject: [PATCH 3/7] Add InvoiceRequestTlvStream::paths Instead of reusing OfferTlvStream::paths, add a dedicated paths TLV to InvoiceRequestTlvStream such that it can be used in Refund. This allows for an Offer without a signing_pubkey and still be able to differentiate whether an invoice is for an offer or a refund. --- lightning/src/offers/invoice.rs | 2 ++ lightning/src/offers/invoice_request.rs | 14 +++++++++- lightning/src/offers/parse.rs | 2 ++ lightning/src/offers/refund.rs | 35 ++++++++++++++++--------- 4 files changed, 39 insertions(+), 14 deletions(-) diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index ab9e7bf8e64..7dd57365321 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -1632,6 +1632,7 @@ mod tests { quantity: None, payer_id: Some(&payer_pubkey()), payer_note: None, + paths: None, }, InvoiceTlvStreamRef { paths: Some(Iterable(payment_paths.iter().map(|(_, path)| path))), @@ -1724,6 +1725,7 @@ mod tests { quantity: None, payer_id: Some(&payer_pubkey()), payer_note: None, + paths: None, }, InvoiceTlvStreamRef { paths: Some(Iterable(payment_paths.iter().map(|(_, path)| path))), diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index a858e5afb2c..be4696be91c 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -971,6 +971,7 @@ impl InvoiceRequestContentsWithoutPayerId { quantity: self.quantity, payer_id: None, payer_note: self.payer_note.as_ref(), + paths: None, }; (payer, offer, invoice_request) @@ -1003,6 +1004,8 @@ pub(super) const INVOICE_REQUEST_TYPES: core::ops::Range = 80..160; /// [`Refund::payer_id`]: crate::offers::refund::Refund::payer_id pub(super) const INVOICE_REQUEST_PAYER_ID_TYPE: u64 = 88; +// This TLV stream is used for both InvoiceRequest and Refund, but not all TLV records are valid for +// InvoiceRequest as noted below. tlv_stream!(InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef, INVOICE_REQUEST_TYPES, { (80, chain: ChainHash), (82, amount: (u64, HighZeroBytesDroppedBigSize)), @@ -1010,6 +1013,8 @@ tlv_stream!(InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef, INVOICE_REQUEST (86, quantity: (u64, HighZeroBytesDroppedBigSize)), (INVOICE_REQUEST_PAYER_ID_TYPE, payer_id: PublicKey), (89, payer_note: (String, WithoutLength)), + // Only used for Refund since the onion message of an InvoiceRequest has a reply path. + (90, paths: (Vec, WithoutLength)), }); type FullInvoiceRequestTlvStream = @@ -1092,7 +1097,9 @@ impl TryFrom for InvoiceRequestContents { let ( PayerTlvStream { metadata }, offer_tlv_stream, - InvoiceRequestTlvStream { chain, amount, features, quantity, payer_id, payer_note }, + InvoiceRequestTlvStream { + chain, amount, features, quantity, payer_id, payer_note, paths, + }, ) = tlv_stream; let payer = match metadata { @@ -1119,6 +1126,10 @@ impl TryFrom for InvoiceRequestContents { Some(payer_id) => payer_id, }; + if paths.is_some() { + return Err(Bolt12SemanticError::UnexpectedPaths); + } + Ok(InvoiceRequestContents { inner: InvoiceRequestContentsWithoutPayerId { payer, offer, chain, amount_msats: amount, features, quantity, payer_note, @@ -1310,6 +1321,7 @@ mod tests { quantity: None, payer_id: Some(&payer_pubkey()), payer_note: None, + paths: None, }, SignatureTlvStreamRef { signature: Some(&invoice_request.signature()) }, ), diff --git a/lightning/src/offers/parse.rs b/lightning/src/offers/parse.rs index 72c4c380d52..3b9b04a5c06 100644 --- a/lightning/src/offers/parse.rs +++ b/lightning/src/offers/parse.rs @@ -183,6 +183,8 @@ pub enum Bolt12SemanticError { DuplicatePaymentId, /// Blinded paths were expected but were missing. MissingPaths, + /// Blinded paths were provided but were not expected. + UnexpectedPaths, /// The blinded payinfo given does not match the number of blinded path hops. InvalidPayInfo, /// An invoice creation time was expected but was missing. diff --git a/lightning/src/offers/refund.rs b/lightning/src/offers/refund.rs index 03253fb6400..f9aa90ba050 100644 --- a/lightning/src/offers/refund.rs +++ b/lightning/src/offers/refund.rs @@ -170,8 +170,8 @@ macro_rules! refund_explicit_metadata_builder_methods { () => { Ok(Self { refund: RefundContents { payer: PayerContents(metadata), description, absolute_expiry: None, issuer: None, - paths: None, chain: None, amount_msats, features: InvoiceRequestFeatures::empty(), - quantity: None, payer_id, payer_note: None, + chain: None, amount_msats, features: InvoiceRequestFeatures::empty(), + quantity: None, payer_id, payer_note: None, paths: None, }, secp_ctx: None, }) @@ -209,8 +209,8 @@ macro_rules! refund_builder_methods { ( Ok(Self { refund: RefundContents { payer: PayerContents(metadata), description, absolute_expiry: None, issuer: None, - paths: None, chain: None, amount_msats, features: InvoiceRequestFeatures::empty(), - quantity: None, payer_id: node_id, payer_note: None, + chain: None, amount_msats, features: InvoiceRequestFeatures::empty(), + quantity: None, payer_id: node_id, payer_note: None, paths: None, }, secp_ctx: Some(secp_ctx), }) @@ -410,7 +410,6 @@ pub(super) struct RefundContents { description: String, absolute_expiry: Option, issuer: Option, - paths: Option>, // invoice_request fields chain: Option, amount_msats: u64, @@ -418,6 +417,7 @@ pub(super) struct RefundContents { quantity: Option, payer_id: PublicKey, payer_note: Option, + paths: Option>, } impl Refund { @@ -734,7 +734,7 @@ impl RefundContents { description: Some(&self.description), features: None, absolute_expiry: self.absolute_expiry.map(|duration| duration.as_secs()), - paths: self.paths.as_ref(), + paths: None, issuer: self.issuer.as_ref(), quantity_max: None, node_id: None, @@ -752,6 +752,7 @@ impl RefundContents { quantity: self.quantity, payer_id: Some(&self.payer_id), payer_note: self.payer_note.as_ref(), + paths: self.paths.as_ref(), }; (payer, offer, invoice_request) @@ -820,9 +821,12 @@ impl TryFrom for RefundContents { PayerTlvStream { metadata: payer_metadata }, OfferTlvStream { chains, metadata, currency, amount: offer_amount, description, - features: offer_features, absolute_expiry, paths, issuer, quantity_max, node_id, + features: offer_features, absolute_expiry, paths: offer_paths, issuer, quantity_max, + node_id, + }, + InvoiceRequestTlvStream { + chain, amount, features, quantity, payer_id, payer_note, paths }, - InvoiceRequestTlvStream { chain, amount, features, quantity, payer_id, payer_note }, ) = tlv_stream; let payer = match payer_metadata { @@ -853,6 +857,10 @@ impl TryFrom for RefundContents { let absolute_expiry = absolute_expiry.map(Duration::from_secs); + if offer_paths.is_some() { + return Err(Bolt12SemanticError::UnexpectedPaths); + } + if quantity_max.is_some() { return Err(Bolt12SemanticError::UnexpectedQuantity); } @@ -877,8 +885,8 @@ impl TryFrom for RefundContents { }; Ok(RefundContents { - payer, description, absolute_expiry, issuer, paths, chain, amount_msats, features, - quantity, payer_id, payer_note, + payer, description, absolute_expiry, issuer, chain, amount_msats, features, quantity, + payer_id, payer_note, paths, }) } } @@ -980,6 +988,7 @@ mod tests { quantity: None, payer_id: Some(&payer_pubkey()), payer_note: None, + paths: None, }, ), ); @@ -1173,12 +1182,12 @@ mod tests { .path(paths[1].clone()) .build() .unwrap(); - let (_, offer_tlv_stream, invoice_request_tlv_stream) = refund.as_tlv_stream(); - assert_eq!(refund.paths(), paths.as_slice()); + let (_, _, invoice_request_tlv_stream) = refund.as_tlv_stream(); assert_eq!(refund.payer_id(), pubkey(42)); + assert_eq!(refund.paths(), paths.as_slice()); assert_ne!(pubkey(42), pubkey(44)); - assert_eq!(offer_tlv_stream.paths, Some(&paths)); assert_eq!(invoice_request_tlv_stream.payer_id, Some(&pubkey(42))); + assert_eq!(invoice_request_tlv_stream.paths, Some(&paths)); } #[test] From b7635c4dc2a5202e64305d65eff36bd74df528d1 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Tue, 23 Apr 2024 18:35:05 -0500 Subject: [PATCH 4/7] Bolt12Invoice for Offer without signing_pubkey When parsing a Bolt12Invoice use both the Offer's signing_pubkey and paths to determine if it is for an Offer or a Refund. Previously, an Offer was required to have a signing_pubkey. But now that it is optional, the Offers paths can be used to make the determination. Additionally, check that the invoice matches one of the blinded node ids from the paths' last hops. --- lightning/src/offers/invoice.rs | 97 ++++++++++++++++++++++++- lightning/src/offers/invoice_request.rs | 15 ++++ 2 files changed, 108 insertions(+), 4 deletions(-) diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index 7dd57365321..75bbcab93f8 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -1434,8 +1434,8 @@ impl TryFrom for InvoiceContents { features, signing_pubkey, }; - match offer_tlv_stream.node_id { - Some(expected_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); } @@ -1445,7 +1445,21 @@ impl TryFrom for InvoiceContents { )?; Ok(InvoiceContents::ForOffer { invoice_request, fields }) }, - None => { + (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) )?; @@ -1463,7 +1477,7 @@ mod tests { use bitcoin::blockdata::script::ScriptBuf; use bitcoin::hashes::Hash; use bitcoin::network::constants::Network; - use bitcoin::secp256k1::{Message, Secp256k1, XOnlyPublicKey, self}; + use bitcoin::secp256k1::{KeyPair, Message, Secp256k1, SecretKey, XOnlyPublicKey, self}; use bitcoin::address::{Address, Payload, WitnessProgram, WitnessVersion}; use bitcoin::key::TweakedPublicKey; @@ -2366,6 +2380,81 @@ mod tests { } } + #[test] + fn parses_invoice_with_node_id_from_blinded_path() { + let paths = vec![ + BlindedPath { + introduction_node: IntroductionNode::NodeId(pubkey(40)), + blinding_point: pubkey(41), + blinded_hops: vec![ + BlindedHop { blinded_node_id: pubkey(43), encrypted_payload: vec![0; 43] }, + BlindedHop { blinded_node_id: pubkey(44), encrypted_payload: vec![0; 44] }, + ], + }, + BlindedPath { + introduction_node: IntroductionNode::NodeId(pubkey(40)), + blinding_point: pubkey(41), + blinded_hops: vec![ + BlindedHop { blinded_node_id: pubkey(45), encrypted_payload: vec![0; 45] }, + BlindedHop { blinded_node_id: pubkey(46), encrypted_payload: vec![0; 46] }, + ], + }, + ]; + + let blinded_node_id_sign = |message: &UnsignedBolt12Invoice| { + let secp_ctx = Secp256k1::new(); + let keys = KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[46; 32]).unwrap()); + Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys)) + }; + + let invoice = OfferBuilder::new("foo".into(), recipient_pubkey()) + .clear_signing_pubkey() + .amount_msats(1000) + .path(paths[0].clone()) + .path(paths[1].clone()) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .build().unwrap() + .sign(payer_sign).unwrap() + .respond_with_no_std_using_signing_pubkey( + payment_paths(), payment_hash(), now(), pubkey(46) + ).unwrap() + .build().unwrap() + .sign(blinded_node_id_sign).unwrap(); + + let mut buffer = Vec::new(); + invoice.write(&mut buffer).unwrap(); + + if let Err(e) = Bolt12Invoice::try_from(buffer) { + panic!("error parsing invoice: {:?}", e); + } + + let invoice = OfferBuilder::new("foo".into(), recipient_pubkey()) + .clear_signing_pubkey() + .amount_msats(1000) + .path(paths[0].clone()) + .path(paths[1].clone()) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .build().unwrap() + .sign(payer_sign).unwrap() + .respond_with_no_std_using_signing_pubkey( + payment_paths(), payment_hash(), now(), recipient_pubkey() + ).unwrap() + .build().unwrap() + .sign(recipient_sign).unwrap(); + + let mut buffer = Vec::new(); + invoice.write(&mut buffer).unwrap(); + + match Bolt12Invoice::try_from(buffer) { + Ok(_) => panic!("expected error"), + Err(e) => { + assert_eq!(e, Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::InvalidSigningPubkey)); + }, + } + } + #[test] fn fails_parsing_invoice_without_signature() { let mut buffer = Vec::new(); diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index be4696be91c..60d1e3c02a2 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -754,6 +754,21 @@ macro_rules! invoice_request_respond_with_explicit_signing_pubkey_methods { ( <$builder>::for_offer(&$contents, payment_paths, created_at, payment_hash, signing_pubkey) } + + #[cfg(test)] + #[allow(dead_code)] + pub(super) fn respond_with_no_std_using_signing_pubkey( + &$self, payment_paths: Vec<(BlindedPayInfo, BlindedPath)>, payment_hash: PaymentHash, + created_at: core::time::Duration, signing_pubkey: PublicKey + ) -> Result<$builder, Bolt12SemanticError> { + debug_assert!($contents.contents.inner.offer.signing_pubkey().is_none()); + + if $contents.invoice_request_features().requires_unknown_bits() { + return Err(Bolt12SemanticError::UnknownRequiredFeatures); + } + + <$builder>::for_offer(&$contents, payment_paths, created_at, payment_hash, signing_pubkey) + } } } macro_rules! invoice_request_verify_method { ($self: ident, $self_type: ty) => { From 48cba2954b245f351c875c36fd22a7173d532ebe Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Wed, 24 Apr 2024 12:40:22 -0500 Subject: [PATCH 5/7] Don't require Offer::description in API Offers currently require a description, though this may change to be optional. Remove the description requirement from the API, setting and empty string by default. --- lightning/src/ln/channelmanager.rs | 9 +- lightning/src/ln/offers_tests.rs | 22 ++-- lightning/src/ln/outbound_payment.rs | 6 +- lightning/src/offers/invoice.rs | 62 ++++++----- lightning/src/offers/invoice_request.rs | 121 +++++++++++---------- lightning/src/offers/merkle.rs | 9 +- lightning/src/offers/offer.rs | 135 ++++++++++++++---------- 7 files changed, 193 insertions(+), 171 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index d10c627916a..e05a3ba46be 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -1554,11 +1554,12 @@ where /// # fn example(channel_manager: T) -> Result<(), Bolt12SemanticError> { /// # let channel_manager = channel_manager.get_cm(); /// let offer = channel_manager -/// .create_offer_builder("coffee".to_string())? +/// .create_offer_builder()? /// # ; /// # // Needed for compiling for c_bindings /// # let builder: lightning::offers::offer::OfferBuilder<_, _> = offer.into(); /// # let offer = builder +/// .description("coffee".to_string()) /// .amount_msats(10_000_000) /// .build()?; /// let bech32_offer = offer.to_string(); @@ -8553,9 +8554,7 @@ macro_rules! create_offer_builder { ($self: ident, $builder: ty) => { /// /// [`Offer`]: crate::offers::offer::Offer /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest - pub fn create_offer_builder( - &$self, description: String - ) -> Result<$builder, Bolt12SemanticError> { + pub fn create_offer_builder(&$self) -> Result<$builder, Bolt12SemanticError> { let node_id = $self.get_our_node_id(); let expanded_key = &$self.inbound_payment_key; let entropy = &*$self.entropy_source; @@ -8563,7 +8562,7 @@ macro_rules! create_offer_builder { ($self: ident, $builder: ty) => { let path = $self.create_blinded_path().map_err(|_| Bolt12SemanticError::MissingPaths)?; let builder = OfferBuilder::deriving_signing_pubkey( - description, node_id, expanded_key, entropy, secp_ctx + node_id, expanded_key, entropy, secp_ctx ) .chain_hash($self.chain_hash) .path(path); diff --git a/lightning/src/ln/offers_tests.rs b/lightning/src/ln/offers_tests.rs index e89e113e8ea..b36e9de39e4 100644 --- a/lightning/src/ln/offers_tests.rs +++ b/lightning/src/ln/offers_tests.rs @@ -268,7 +268,7 @@ fn prefers_non_tor_nodes_in_blinded_paths() { announce_node_address(charlie, &[alice, bob, david, &nodes[4], &nodes[5]], tor.clone()); let offer = bob.node - .create_offer_builder("coffee".to_string()).unwrap() + .create_offer_builder().unwrap() .amount_msats(10_000_000) .build().unwrap(); assert_ne!(offer.signing_pubkey(), Some(bob_id)); @@ -283,7 +283,7 @@ fn prefers_non_tor_nodes_in_blinded_paths() { announce_node_address(&nodes[5], &[alice, bob, charlie, david, &nodes[4]], tor.clone()); let offer = bob.node - .create_offer_builder("coffee".to_string()).unwrap() + .create_offer_builder().unwrap() .amount_msats(10_000_000) .build().unwrap(); assert_ne!(offer.signing_pubkey(), Some(bob_id)); @@ -333,7 +333,7 @@ fn prefers_more_connected_nodes_in_blinded_paths() { disconnect_peers(david, &[bob, &nodes[4], &nodes[5]]); let offer = bob.node - .create_offer_builder("coffee".to_string()).unwrap() + .create_offer_builder().unwrap() .amount_msats(10_000_000) .build().unwrap(); assert_ne!(offer.signing_pubkey(), Some(bob_id)); @@ -382,7 +382,7 @@ fn creates_and_pays_for_offer_using_two_hop_blinded_path() { disconnect_peers(david, &[bob, &nodes[4], &nodes[5]]); let offer = alice.node - .create_offer_builder("coffee".to_string()) + .create_offer_builder() .unwrap() .amount_msats(10_000_000) .build().unwrap(); @@ -544,7 +544,7 @@ fn creates_and_pays_for_offer_using_one_hop_blinded_path() { let bob_id = bob.node.get_our_node_id(); let offer = alice.node - .create_offer_builder("coffee".to_string()).unwrap() + .create_offer_builder().unwrap() .amount_msats(10_000_000) .build().unwrap(); assert_ne!(offer.signing_pubkey(), Some(alice_id)); @@ -668,7 +668,7 @@ fn pays_for_offer_without_blinded_paths() { let bob_id = bob.node.get_our_node_id(); let offer = alice.node - .create_offer_builder("coffee".to_string()).unwrap() + .create_offer_builder().unwrap() .clear_paths() .amount_msats(10_000_000) .build().unwrap(); @@ -760,7 +760,7 @@ fn fails_creating_offer_without_blinded_paths() { create_unannounced_chan_between_nodes_with_value(&nodes, 0, 1, 10_000_000, 1_000_000_000); - match nodes[0].node.create_offer_builder("coffee".to_string()) { + match nodes[0].node.create_offer_builder() { Ok(_) => panic!("Expected error"), Err(e) => assert_eq!(e, Bolt12SemanticError::MissingPaths), } @@ -803,7 +803,7 @@ fn fails_creating_invoice_request_for_unsupported_chain() { let bob = &nodes[1]; let offer = alice.node - .create_offer_builder("coffee".to_string()).unwrap() + .create_offer_builder().unwrap() .clear_chains() .chain(Network::Signet) .build().unwrap(); @@ -865,7 +865,7 @@ fn fails_creating_invoice_request_without_blinded_reply_path() { disconnect_peers(david, &[bob, &nodes[4], &nodes[5]]); let offer = alice.node - .create_offer_builder("coffee".to_string()).unwrap() + .create_offer_builder().unwrap() .amount_msats(10_000_000) .build().unwrap(); @@ -899,7 +899,7 @@ fn fails_creating_invoice_request_with_duplicate_payment_id() { disconnect_peers(alice, &[charlie, david, &nodes[4], &nodes[5]]); let offer = alice.node - .create_offer_builder("coffee".to_string()).unwrap() + .create_offer_builder().unwrap() .amount_msats(10_000_000) .build().unwrap(); @@ -985,7 +985,7 @@ fn fails_sending_invoice_without_blinded_payment_paths_for_offer() { disconnect_peers(david, &[bob, &nodes[4], &nodes[5]]); let offer = alice.node - .create_offer_builder("coffee".to_string()).unwrap() + .create_offer_builder().unwrap() .amount_msats(10_000_000) .build().unwrap(); diff --git a/lightning/src/ln/outbound_payment.rs b/lightning/src/ln/outbound_payment.rs index b05d6f3f729..f37a7450729 100644 --- a/lightning/src/ln/outbound_payment.rs +++ b/lightning/src/ln/outbound_payment.rs @@ -2194,7 +2194,7 @@ mod tests { assert!(outbound_payments.has_pending_payments()); let created_at = now() - DEFAULT_RELATIVE_EXPIRY; - let invoice = OfferBuilder::new("foo".into(), recipient_pubkey()) + let invoice = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .build().unwrap() .request_invoice(vec![1; 32], payer_pubkey()).unwrap() @@ -2237,7 +2237,7 @@ mod tests { let payment_id = PaymentId([0; 32]); let expiration = StaleExpiration::AbsoluteTimeout(Duration::from_secs(100)); - let invoice = OfferBuilder::new("foo".into(), recipient_pubkey()) + let invoice = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .build().unwrap() .request_invoice(vec![1; 32], payer_pubkey()).unwrap() @@ -2296,7 +2296,7 @@ mod tests { let payment_id = PaymentId([0; 32]); let expiration = StaleExpiration::AbsoluteTimeout(Duration::from_secs(100)); - let invoice = OfferBuilder::new("foo".into(), recipient_pubkey()) + let invoice = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .build().unwrap() .request_invoice(vec![1; 32], payer_pubkey()).unwrap() diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index 75bbcab93f8..647da4c4f3a 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -1529,7 +1529,7 @@ mod tests { let payment_paths = payment_paths(); let payment_hash = payment_hash(); let now = now(); - let unsigned_invoice = OfferBuilder::new("foo".into(), recipient_pubkey()) + let unsigned_invoice = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .build().unwrap() .request_invoice(vec![1; 32], payer_pubkey()).unwrap() @@ -1546,7 +1546,7 @@ mod tests { assert_eq!(unsigned_invoice.offer_chains(), Some(vec![ChainHash::using_genesis_block(Network::Bitcoin)])); assert_eq!(unsigned_invoice.metadata(), None); assert_eq!(unsigned_invoice.amount(), Some(&Amount::Bitcoin { amount_msats: 1000 })); - assert_eq!(unsigned_invoice.description(), PrintableString("foo")); + assert_eq!(unsigned_invoice.description(), PrintableString("")); assert_eq!(unsigned_invoice.offer_features(), Some(&OfferFeatures::empty())); assert_eq!(unsigned_invoice.absolute_expiry(), None); assert_eq!(unsigned_invoice.message_paths(), &[]); @@ -1590,7 +1590,7 @@ mod tests { assert_eq!(invoice.offer_chains(), Some(vec![ChainHash::using_genesis_block(Network::Bitcoin)])); assert_eq!(invoice.metadata(), None); assert_eq!(invoice.amount(), Some(&Amount::Bitcoin { amount_msats: 1000 })); - assert_eq!(invoice.description(), PrintableString("foo")); + assert_eq!(invoice.description(), PrintableString("")); assert_eq!(invoice.offer_features(), Some(&OfferFeatures::empty())); assert_eq!(invoice.absolute_expiry(), None); assert_eq!(invoice.message_paths(), &[]); @@ -1631,7 +1631,7 @@ mod tests { metadata: None, currency: None, amount: Some(1000), - description: Some(&String::from("foo")), + description: Some(&String::from("")), features: None, absolute_expiry: None, paths: None, @@ -1767,7 +1767,7 @@ mod tests { let future_expiry = Duration::from_secs(u64::max_value()); let past_expiry = Duration::from_secs(0); - if let Err(e) = OfferBuilder::new("foo".into(), recipient_pubkey()) + if let Err(e) = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .absolute_expiry(future_expiry) .build().unwrap() @@ -1781,7 +1781,7 @@ mod tests { panic!("error building invoice: {:?}", e); } - match OfferBuilder::new("foo".into(), recipient_pubkey()) + match OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .absolute_expiry(past_expiry) .build().unwrap() @@ -1827,7 +1827,6 @@ mod tests { #[test] fn builds_invoice_from_offer_using_derived_keys() { - let desc = "foo".to_string(); let node_id = recipient_pubkey(); let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32])); let entropy = FixedEntropy {}; @@ -1845,7 +1844,7 @@ mod tests { #[cfg(c_bindings)] use crate::offers::offer::OfferWithDerivedMetadataBuilder as OfferBuilder; let offer = OfferBuilder - ::deriving_signing_pubkey(desc, node_id, &expanded_key, &entropy, &secp_ctx) + ::deriving_signing_pubkey(node_id, &expanded_key, &entropy, &secp_ctx) .amount_msats(1000) .path(blinded_path) .build().unwrap(); @@ -1864,9 +1863,8 @@ mod tests { let expanded_key = ExpandedKey::new(&KeyMaterial([41; 32])); assert!(invoice_request.verify(&expanded_key, &secp_ctx).is_err()); - let desc = "foo".to_string(); let offer = OfferBuilder - ::deriving_signing_pubkey(desc, node_id, &expanded_key, &entropy, &secp_ctx) + ::deriving_signing_pubkey(node_id, &expanded_key, &entropy, &secp_ctx) .amount_msats(1000) // Omit the path so that node_id is used for the signing pubkey instead of deriving .build().unwrap(); @@ -1908,7 +1906,7 @@ mod tests { let now = now(); let one_hour = Duration::from_secs(3600); - let invoice = OfferBuilder::new("foo".into(), recipient_pubkey()) + let invoice = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .build().unwrap() .request_invoice(vec![1; 32], payer_pubkey()).unwrap() @@ -1924,7 +1922,7 @@ mod tests { assert_eq!(invoice.relative_expiry(), one_hour); assert_eq!(tlv_stream.relative_expiry, Some(one_hour.as_secs() as u32)); - let invoice = OfferBuilder::new("foo".into(), recipient_pubkey()) + let invoice = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .build().unwrap() .request_invoice(vec![1; 32], payer_pubkey()).unwrap() @@ -1943,7 +1941,7 @@ mod tests { #[test] fn builds_invoice_with_amount_from_request() { - let invoice = OfferBuilder::new("foo".into(), recipient_pubkey()) + let invoice = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .build().unwrap() .request_invoice(vec![1; 32], payer_pubkey()).unwrap() @@ -1960,7 +1958,7 @@ mod tests { #[test] fn builds_invoice_with_quantity_from_request() { - let invoice = OfferBuilder::new("foo".into(), recipient_pubkey()) + let invoice = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .supported_quantity(Quantity::Unbounded) .build().unwrap() @@ -1975,7 +1973,7 @@ mod tests { assert_eq!(invoice.amount_msats(), 2000); assert_eq!(tlv_stream.amount, Some(2000)); - match OfferBuilder::new("foo".into(), recipient_pubkey()) + match OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .supported_quantity(Quantity::Unbounded) .build().unwrap() @@ -1997,7 +1995,7 @@ mod tests { let x_only_pubkey = XOnlyPublicKey::from_keypair(&recipient_keys()).0; let tweaked_pubkey = TweakedPublicKey::dangerous_assume_tweaked(x_only_pubkey); - let invoice = OfferBuilder::new("foo".into(), recipient_pubkey()) + let invoice = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .build().unwrap() .request_invoice(vec![1; 32], payer_pubkey()).unwrap() @@ -2042,7 +2040,7 @@ mod tests { let mut features = Bolt12InvoiceFeatures::empty(); features.set_basic_mpp_optional(); - let invoice = OfferBuilder::new("foo".into(), recipient_pubkey()) + let invoice = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .build().unwrap() .request_invoice(vec![1; 32], payer_pubkey()).unwrap() @@ -2059,7 +2057,7 @@ mod tests { #[test] fn fails_signing_invoice() { - match OfferBuilder::new("foo".into(), recipient_pubkey()) + match OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .build().unwrap() .request_invoice(vec![1; 32], payer_pubkey()).unwrap() @@ -2073,7 +2071,7 @@ mod tests { Err(e) => assert_eq!(e, SignError::Signing), } - match OfferBuilder::new("foo".into(), recipient_pubkey()) + match OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .build().unwrap() .request_invoice(vec![1; 32], payer_pubkey()).unwrap() @@ -2090,7 +2088,7 @@ mod tests { #[test] fn parses_invoice_with_payment_paths() { - let invoice = OfferBuilder::new("foo".into(), recipient_pubkey()) + let invoice = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .build().unwrap() .request_invoice(vec![1; 32], payer_pubkey()).unwrap() @@ -2145,7 +2143,7 @@ mod tests { #[test] fn parses_invoice_with_created_at() { - let invoice = OfferBuilder::new("foo".into(), recipient_pubkey()) + let invoice = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .build().unwrap() .request_invoice(vec![1; 32], payer_pubkey()).unwrap() @@ -2175,7 +2173,7 @@ mod tests { #[test] fn parses_invoice_with_relative_expiry() { - let invoice = OfferBuilder::new("foo".into(), recipient_pubkey()) + let invoice = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .build().unwrap() .request_invoice(vec![1; 32], payer_pubkey()).unwrap() @@ -2197,7 +2195,7 @@ mod tests { #[test] fn parses_invoice_with_payment_hash() { - let invoice = OfferBuilder::new("foo".into(), recipient_pubkey()) + let invoice = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .build().unwrap() .request_invoice(vec![1; 32], payer_pubkey()).unwrap() @@ -2227,7 +2225,7 @@ mod tests { #[test] fn parses_invoice_with_amount() { - let invoice = OfferBuilder::new("foo".into(), recipient_pubkey()) + let invoice = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .build().unwrap() .request_invoice(vec![1; 32], payer_pubkey()).unwrap() @@ -2255,7 +2253,7 @@ mod tests { #[test] fn parses_invoice_with_allow_mpp() { - let invoice = OfferBuilder::new("foo".into(), recipient_pubkey()) + let invoice = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .build().unwrap() .request_invoice(vec![1; 32], payer_pubkey()).unwrap() @@ -2286,7 +2284,7 @@ mod tests { let x_only_pubkey = XOnlyPublicKey::from_keypair(&recipient_keys()).0; let tweaked_pubkey = TweakedPublicKey::dangerous_assume_tweaked(x_only_pubkey); - let offer = OfferBuilder::new("foo".into(), recipient_pubkey()) + let offer = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .build().unwrap(); let invoice_request = offer @@ -2341,7 +2339,7 @@ mod tests { #[test] fn parses_invoice_with_node_id() { - let invoice = OfferBuilder::new("foo".into(), recipient_pubkey()) + let invoice = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .build().unwrap() .request_invoice(vec![1; 32], payer_pubkey()).unwrap() @@ -2407,7 +2405,7 @@ mod tests { Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys)) }; - let invoice = OfferBuilder::new("foo".into(), recipient_pubkey()) + let invoice = OfferBuilder::new(recipient_pubkey()) .clear_signing_pubkey() .amount_msats(1000) .path(paths[0].clone()) @@ -2429,7 +2427,7 @@ mod tests { panic!("error parsing invoice: {:?}", e); } - let invoice = OfferBuilder::new("foo".into(), recipient_pubkey()) + let invoice = OfferBuilder::new(recipient_pubkey()) .clear_signing_pubkey() .amount_msats(1000) .path(paths[0].clone()) @@ -2458,7 +2456,7 @@ mod tests { #[test] fn fails_parsing_invoice_without_signature() { let mut buffer = Vec::new(); - OfferBuilder::new("foo".into(), recipient_pubkey()) + OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .build().unwrap() .request_invoice(vec![1; 32], payer_pubkey()).unwrap() @@ -2477,7 +2475,7 @@ mod tests { #[test] fn fails_parsing_invoice_with_invalid_signature() { - let mut invoice = OfferBuilder::new("foo".into(), recipient_pubkey()) + let mut invoice = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .build().unwrap() .request_invoice(vec![1; 32], payer_pubkey()).unwrap() @@ -2502,7 +2500,7 @@ mod tests { #[test] fn fails_parsing_invoice_with_extra_tlv_records() { - let invoice = OfferBuilder::new("foo".into(), recipient_pubkey()) + let invoice = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .build().unwrap() .request_invoice(vec![1; 32], payer_pubkey()).unwrap() diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index 60d1e3c02a2..90d6d5bf308 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -1247,7 +1247,7 @@ mod tests { #[test] fn builds_invoice_request_with_defaults() { - let unsigned_invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey()) + let unsigned_invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .build().unwrap() .request_invoice(vec![1; 32], payer_pubkey()).unwrap() @@ -1263,7 +1263,7 @@ mod tests { assert_eq!(unsigned_invoice_request.chains(), vec![ChainHash::using_genesis_block(Network::Bitcoin)]); assert_eq!(unsigned_invoice_request.metadata(), None); assert_eq!(unsigned_invoice_request.amount(), Some(&Amount::Bitcoin { amount_msats: 1000 })); - assert_eq!(unsigned_invoice_request.description(), PrintableString("foo")); + assert_eq!(unsigned_invoice_request.description(), PrintableString("")); assert_eq!(unsigned_invoice_request.offer_features(), &OfferFeatures::empty()); assert_eq!(unsigned_invoice_request.absolute_expiry(), None); assert_eq!(unsigned_invoice_request.paths(), &[]); @@ -1295,7 +1295,7 @@ mod tests { assert_eq!(invoice_request.chains(), vec![ChainHash::using_genesis_block(Network::Bitcoin)]); assert_eq!(invoice_request.metadata(), None); assert_eq!(invoice_request.amount(), Some(&Amount::Bitcoin { amount_msats: 1000 })); - assert_eq!(invoice_request.description(), PrintableString("foo")); + assert_eq!(invoice_request.description(), PrintableString("")); assert_eq!(invoice_request.offer_features(), &OfferFeatures::empty()); assert_eq!(invoice_request.absolute_expiry(), None); assert_eq!(invoice_request.paths(), &[]); @@ -1321,7 +1321,7 @@ mod tests { metadata: None, currency: None, amount: Some(1000), - description: Some(&String::from("foo")), + description: Some(&String::from("")), features: None, absolute_expiry: None, paths: None, @@ -1353,7 +1353,7 @@ mod tests { let future_expiry = Duration::from_secs(u64::max_value()); let past_expiry = Duration::from_secs(0); - if let Err(e) = OfferBuilder::new("foo".into(), recipient_pubkey()) + if let Err(e) = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .absolute_expiry(future_expiry) .build().unwrap() @@ -1363,7 +1363,7 @@ mod tests { panic!("error building invoice_request: {:?}", e); } - match OfferBuilder::new("foo".into(), recipient_pubkey()) + match OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .absolute_expiry(past_expiry) .build().unwrap() @@ -1383,7 +1383,7 @@ mod tests { let secp_ctx = Secp256k1::new(); let payment_id = PaymentId([1; 32]); - let offer = OfferBuilder::new("foo".into(), recipient_pubkey()) + let offer = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .build().unwrap(); let invoice_request = offer @@ -1456,7 +1456,7 @@ mod tests { let secp_ctx = Secp256k1::new(); let payment_id = PaymentId([1; 32]); - let offer = OfferBuilder::new("foo".into(), recipient_pubkey()) + let offer = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .build().unwrap(); let invoice_request = offer @@ -1526,7 +1526,7 @@ mod tests { let mainnet = ChainHash::using_genesis_block(Network::Bitcoin); let testnet = ChainHash::using_genesis_block(Network::Testnet); - let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey()) + let invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .build().unwrap() .request_invoice(vec![1; 32], payer_pubkey()).unwrap() @@ -1537,7 +1537,7 @@ mod tests { assert_eq!(invoice_request.chain(), mainnet); assert_eq!(tlv_stream.chain, None); - let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey()) + let invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .chain(Network::Testnet) .build().unwrap() @@ -1549,7 +1549,7 @@ mod tests { assert_eq!(invoice_request.chain(), testnet); assert_eq!(tlv_stream.chain, Some(&testnet)); - let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey()) + let invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .chain(Network::Bitcoin) .chain(Network::Testnet) @@ -1562,7 +1562,7 @@ mod tests { assert_eq!(invoice_request.chain(), mainnet); assert_eq!(tlv_stream.chain, None); - let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey()) + let invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .chain(Network::Bitcoin) .chain(Network::Testnet) @@ -1576,7 +1576,7 @@ mod tests { assert_eq!(invoice_request.chain(), testnet); assert_eq!(tlv_stream.chain, Some(&testnet)); - match OfferBuilder::new("foo".into(), recipient_pubkey()) + match OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .chain(Network::Testnet) .build().unwrap() @@ -1587,7 +1587,7 @@ mod tests { Err(e) => assert_eq!(e, Bolt12SemanticError::UnsupportedChain), } - match OfferBuilder::new("foo".into(), recipient_pubkey()) + match OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .chain(Network::Testnet) .build().unwrap() @@ -1601,7 +1601,7 @@ mod tests { #[test] fn builds_invoice_request_with_amount() { - let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey()) + let invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .build().unwrap() .request_invoice(vec![1; 32], payer_pubkey()).unwrap() @@ -1612,7 +1612,7 @@ mod tests { assert_eq!(invoice_request.amount_msats(), Some(1000)); assert_eq!(tlv_stream.amount, Some(1000)); - let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey()) + let invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .build().unwrap() .request_invoice(vec![1; 32], payer_pubkey()).unwrap() @@ -1624,7 +1624,7 @@ mod tests { assert_eq!(invoice_request.amount_msats(), Some(1000)); assert_eq!(tlv_stream.amount, Some(1000)); - let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey()) + let invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .build().unwrap() .request_invoice(vec![1; 32], payer_pubkey()).unwrap() @@ -1635,7 +1635,7 @@ mod tests { assert_eq!(invoice_request.amount_msats(), Some(1001)); assert_eq!(tlv_stream.amount, Some(1001)); - match OfferBuilder::new("foo".into(), recipient_pubkey()) + match OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .build().unwrap() .request_invoice(vec![1; 32], payer_pubkey()).unwrap() @@ -1645,7 +1645,7 @@ mod tests { Err(e) => assert_eq!(e, Bolt12SemanticError::InsufficientAmount), } - match OfferBuilder::new("foo".into(), recipient_pubkey()) + match OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .supported_quantity(Quantity::Unbounded) .build().unwrap() @@ -1657,7 +1657,7 @@ mod tests { Err(e) => assert_eq!(e, Bolt12SemanticError::InsufficientAmount), } - match OfferBuilder::new("foo".into(), recipient_pubkey()) + match OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .build().unwrap() .request_invoice(vec![1; 32], payer_pubkey()).unwrap() @@ -1667,7 +1667,7 @@ mod tests { Err(e) => assert_eq!(e, Bolt12SemanticError::InvalidAmount), } - match OfferBuilder::new("foo".into(), recipient_pubkey()) + match OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .supported_quantity(Quantity::Unbounded) .build().unwrap() @@ -1680,7 +1680,7 @@ mod tests { Err(e) => assert_eq!(e, Bolt12SemanticError::InsufficientAmount), } - match OfferBuilder::new("foo".into(), recipient_pubkey()) + match OfferBuilder::new(recipient_pubkey()) .build().unwrap() .request_invoice(vec![1; 32], payer_pubkey()).unwrap() .build() @@ -1689,7 +1689,7 @@ mod tests { Err(e) => assert_eq!(e, Bolt12SemanticError::MissingAmount), } - match OfferBuilder::new("foo".into(), recipient_pubkey()) + match OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .supported_quantity(Quantity::Unbounded) .build().unwrap() @@ -1704,7 +1704,7 @@ mod tests { #[test] fn builds_invoice_request_with_features() { - let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey()) + let invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .build().unwrap() .request_invoice(vec![1; 32], payer_pubkey()).unwrap() @@ -1715,7 +1715,7 @@ mod tests { assert_eq!(invoice_request.invoice_request_features(), &InvoiceRequestFeatures::unknown()); assert_eq!(tlv_stream.features, Some(&InvoiceRequestFeatures::unknown())); - let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey()) + let invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .build().unwrap() .request_invoice(vec![1; 32], payer_pubkey()).unwrap() @@ -1733,7 +1733,7 @@ mod tests { let one = NonZeroU64::new(1).unwrap(); let ten = NonZeroU64::new(10).unwrap(); - let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey()) + let invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .supported_quantity(Quantity::One) .build().unwrap() @@ -1744,7 +1744,7 @@ mod tests { assert_eq!(invoice_request.quantity(), None); assert_eq!(tlv_stream.quantity, None); - match OfferBuilder::new("foo".into(), recipient_pubkey()) + match OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .supported_quantity(Quantity::One) .build().unwrap() @@ -1756,7 +1756,7 @@ mod tests { Err(e) => assert_eq!(e, Bolt12SemanticError::UnexpectedQuantity), } - let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey()) + let invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .supported_quantity(Quantity::Bounded(ten)) .build().unwrap() @@ -1769,7 +1769,7 @@ mod tests { assert_eq!(invoice_request.amount_msats(), Some(10_000)); assert_eq!(tlv_stream.amount, Some(10_000)); - match OfferBuilder::new("foo".into(), recipient_pubkey()) + match OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .supported_quantity(Quantity::Bounded(ten)) .build().unwrap() @@ -1781,7 +1781,7 @@ mod tests { Err(e) => assert_eq!(e, Bolt12SemanticError::InvalidQuantity), } - let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey()) + let invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .supported_quantity(Quantity::Unbounded) .build().unwrap() @@ -1794,7 +1794,7 @@ mod tests { assert_eq!(invoice_request.amount_msats(), Some(2_000)); assert_eq!(tlv_stream.amount, Some(2_000)); - match OfferBuilder::new("foo".into(), recipient_pubkey()) + match OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .supported_quantity(Quantity::Unbounded) .build().unwrap() @@ -1805,7 +1805,7 @@ mod tests { Err(e) => assert_eq!(e, Bolt12SemanticError::MissingQuantity), } - match OfferBuilder::new("foo".into(), recipient_pubkey()) + match OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .supported_quantity(Quantity::Bounded(one)) .build().unwrap() @@ -1819,7 +1819,7 @@ mod tests { #[test] fn builds_invoice_request_with_payer_note() { - let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey()) + let invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .build().unwrap() .request_invoice(vec![1; 32], payer_pubkey()).unwrap() @@ -1830,7 +1830,7 @@ mod tests { assert_eq!(invoice_request.payer_note(), Some(PrintableString("bar"))); assert_eq!(tlv_stream.payer_note, Some(&String::from("bar"))); - let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey()) + let invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .build().unwrap() .request_invoice(vec![1; 32], payer_pubkey()).unwrap() @@ -1845,7 +1845,7 @@ mod tests { #[test] fn fails_signing_invoice_request() { - match OfferBuilder::new("foo".into(), recipient_pubkey()) + match OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .build().unwrap() .request_invoice(vec![1; 32], payer_pubkey()).unwrap() @@ -1856,7 +1856,7 @@ mod tests { Err(e) => assert_eq!(e, SignError::Signing), } - match OfferBuilder::new("foo".into(), recipient_pubkey()) + match OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .build().unwrap() .request_invoice(vec![1; 32], payer_pubkey()).unwrap() @@ -1870,7 +1870,7 @@ mod tests { #[test] fn fails_responding_with_unknown_required_features() { - match OfferBuilder::new("foo".into(), recipient_pubkey()) + match OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .build().unwrap() .request_invoice(vec![42; 32], payer_pubkey()).unwrap() @@ -1886,7 +1886,7 @@ mod tests { #[test] fn parses_invoice_request_with_metadata() { - let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey()) + let invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .build().unwrap() .request_invoice(vec![1; 32], payer_pubkey()).unwrap() @@ -1903,7 +1903,7 @@ mod tests { #[test] fn parses_invoice_request_with_chain() { - let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey()) + let invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .build().unwrap() .request_invoice(vec![1; 32], payer_pubkey()).unwrap() @@ -1918,7 +1918,7 @@ mod tests { panic!("error parsing invoice_request: {:?}", e); } - let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey()) + let invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .build().unwrap() .request_invoice(vec![1; 32], payer_pubkey()).unwrap() @@ -1937,7 +1937,7 @@ mod tests { #[test] fn parses_invoice_request_with_amount() { - let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey()) + let invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .build().unwrap() .request_invoice(vec![1; 32], payer_pubkey()).unwrap() @@ -1951,7 +1951,7 @@ mod tests { panic!("error parsing invoice_request: {:?}", e); } - let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey()) + let invoice_request = OfferBuilder::new(recipient_pubkey()) .build().unwrap() .request_invoice(vec![1; 32], payer_pubkey()).unwrap() .amount_msats(1000).unwrap() @@ -1965,7 +1965,7 @@ mod tests { panic!("error parsing invoice_request: {:?}", e); } - let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey()) + let invoice_request = OfferBuilder::new(recipient_pubkey()) .build().unwrap() .request_invoice(vec![1; 32], payer_pubkey()).unwrap() .build_unchecked() @@ -1979,7 +1979,7 @@ mod tests { Err(e) => assert_eq!(e, Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::MissingAmount)), } - let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey()) + let invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .build().unwrap() .request_invoice(vec![1; 32], payer_pubkey()).unwrap() @@ -1995,7 +1995,7 @@ mod tests { Err(e) => assert_eq!(e, Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::InsufficientAmount)), } - let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey()) + let invoice_request = OfferBuilder::new(recipient_pubkey()) .amount(Amount::Currency { iso4217_code: *b"USD", amount: 1000 }) .build_unchecked() .request_invoice(vec![1; 32], payer_pubkey()).unwrap() @@ -2012,7 +2012,7 @@ mod tests { }, } - let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey()) + let invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .supported_quantity(Quantity::Unbounded) .build().unwrap() @@ -2035,7 +2035,7 @@ mod tests { let one = NonZeroU64::new(1).unwrap(); let ten = NonZeroU64::new(10).unwrap(); - let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey()) + let invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .supported_quantity(Quantity::One) .build().unwrap() @@ -2050,7 +2050,7 @@ mod tests { panic!("error parsing invoice_request: {:?}", e); } - let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey()) + let invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .supported_quantity(Quantity::One) .build().unwrap() @@ -2070,7 +2070,7 @@ mod tests { }, } - let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey()) + let invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .supported_quantity(Quantity::Bounded(ten)) .build().unwrap() @@ -2087,7 +2087,7 @@ mod tests { panic!("error parsing invoice_request: {:?}", e); } - let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey()) + let invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .supported_quantity(Quantity::Bounded(ten)) .build().unwrap() @@ -2105,7 +2105,7 @@ mod tests { Err(e) => assert_eq!(e, Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::InvalidQuantity)), } - let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey()) + let invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .supported_quantity(Quantity::Unbounded) .build().unwrap() @@ -2122,7 +2122,7 @@ mod tests { panic!("error parsing invoice_request: {:?}", e); } - let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey()) + let invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .supported_quantity(Quantity::Unbounded) .build().unwrap() @@ -2138,7 +2138,7 @@ mod tests { Err(e) => assert_eq!(e, Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::MissingQuantity)), } - let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey()) + let invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .supported_quantity(Quantity::Bounded(one)) .build().unwrap() @@ -2157,7 +2157,7 @@ mod tests { #[test] fn fails_parsing_invoice_request_without_metadata() { - let offer = OfferBuilder::new("foo".into(), recipient_pubkey()) + let offer = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .build().unwrap(); let unsigned_invoice_request = offer.request_invoice(vec![1; 32], payer_pubkey()).unwrap() @@ -2178,7 +2178,7 @@ mod tests { #[test] fn fails_parsing_invoice_request_without_payer_id() { - let offer = OfferBuilder::new("foo".into(), recipient_pubkey()) + let offer = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .build().unwrap(); let unsigned_invoice_request = offer.request_invoice(vec![1; 32], payer_pubkey()).unwrap() @@ -2197,7 +2197,7 @@ mod tests { #[test] fn fails_parsing_invoice_request_without_node_id() { - let offer = OfferBuilder::new("foo".into(), recipient_pubkey()) + let offer = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .build().unwrap(); let unsigned_invoice_request = offer.request_invoice(vec![1; 32], payer_pubkey()).unwrap() @@ -2219,7 +2219,7 @@ mod tests { #[test] fn fails_parsing_invoice_request_without_signature() { let mut buffer = Vec::new(); - OfferBuilder::new("foo".into(), recipient_pubkey()) + OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .build().unwrap() .request_invoice(vec![1; 32], payer_pubkey()).unwrap() @@ -2235,7 +2235,7 @@ mod tests { #[test] fn fails_parsing_invoice_request_with_invalid_signature() { - let mut invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey()) + let mut invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .build().unwrap() .request_invoice(vec![1; 32], payer_pubkey()).unwrap() @@ -2259,7 +2259,7 @@ mod tests { fn fails_parsing_invoice_request_with_extra_tlv_records() { let secp_ctx = Secp256k1::new(); let keys = KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap()); - let invoice_request = OfferBuilder::new("foo".into(), keys.public_key()) + let invoice_request = OfferBuilder::new(keys.public_key()) .amount_msats(1000) .build().unwrap() .request_invoice(vec![1; 32], keys.public_key()).unwrap() @@ -2283,7 +2283,6 @@ mod tests { #[test] fn copies_verified_invoice_request_fields() { - let desc = "foo".to_string(); let node_id = recipient_pubkey(); let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32])); let entropy = FixedEntropy {}; @@ -2292,7 +2291,7 @@ mod tests { #[cfg(c_bindings)] use crate::offers::offer::OfferWithDerivedMetadataBuilder as OfferBuilder; let offer = OfferBuilder - ::deriving_signing_pubkey(desc, node_id, &expanded_key, &entropy, &secp_ctx) + ::deriving_signing_pubkey(node_id, &expanded_key, &entropy, &secp_ctx) .chain(Network::Testnet) .amount_msats(1000) .supported_quantity(Quantity::Unbounded) diff --git a/lightning/src/offers/merkle.rs b/lightning/src/offers/merkle.rs index a3979866926..d8d1a90e3fd 100644 --- a/lightning/src/offers/merkle.rs +++ b/lightning/src/offers/merkle.rs @@ -346,7 +346,8 @@ mod tests { }; // BOLT 12 test vectors - let invoice_request = OfferBuilder::new("A Mathematical Treatise".into(), recipient_pubkey) + let invoice_request = OfferBuilder::new(recipient_pubkey) + .description("A Mathematical Treatise".into()) .amount(Amount::Currency { iso4217_code: *b"USD", amount: 100 }) .build_unchecked() .request_invoice(vec![0; 8], payer_keys.public_key()).unwrap() @@ -371,7 +372,7 @@ mod tests { #[test] fn compute_tagged_hash() { - let unsigned_invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey()) + let unsigned_invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .build().unwrap() .request_invoice(vec![1; 32], payer_pubkey()).unwrap() @@ -400,7 +401,7 @@ mod tests { KeyPair::from_secret_key(&secp_ctx, &secret_key) }; - let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey) + let invoice_request = OfferBuilder::new(recipient_pubkey) .amount_msats(100) .build_unchecked() .request_invoice(vec![0; 8], payer_keys.public_key()).unwrap() @@ -432,7 +433,7 @@ mod tests { KeyPair::from_secret_key(&secp_ctx, &secret_key) }; - let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey) + let invoice_request = OfferBuilder::new(recipient_pubkey) .amount_msats(100) .build_unchecked() .request_invoice(vec![0; 8], payer_keys.public_key()).unwrap() diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs index 8e93a93f873..baac949515a 100644 --- a/lightning/src/offers/offer.rs +++ b/lightning/src/offers/offer.rs @@ -43,7 +43,8 @@ //! let pubkey = PublicKey::from(keys); //! //! let expiration = SystemTime::now() + Duration::from_secs(24 * 60 * 60); -//! let offer = OfferBuilder::new("coffee, large".to_string(), pubkey) +//! let offer = OfferBuilder::new(pubkey) +//! .description("coffee, large".to_string()) //! .amount_msats(20_000) //! .supported_quantity(Quantity::Unbounded) //! .absolute_expiry(expiration.duration_since(SystemTime::UNIX_EPOCH).unwrap()) @@ -208,7 +209,7 @@ impl MetadataStrategy for DerivedMetadata {} macro_rules! offer_explicit_metadata_builder_methods { ( $self: ident, $self_type: ty, $return_type: ty, $return_value: expr ) => { - /// Creates a new builder for an offer setting the [`Offer::description`] and using the + /// Creates a new builder for an offer setting an empty [`Offer::description`] and using the /// [`Offer::signing_pubkey`] for signing invoices. The associated secret key must be remembered /// while the offer is valid. /// @@ -221,10 +222,10 @@ macro_rules! offer_explicit_metadata_builder_methods { ( /// /// [`ChannelManager`]: crate::ln::channelmanager::ChannelManager /// [`ChannelManager::create_offer_builder`]: crate::ln::channelmanager::ChannelManager::create_offer_builder - pub fn new(description: String, signing_pubkey: PublicKey) -> Self { + pub fn new(signing_pubkey: PublicKey) -> Self { Self { offer: OfferContents { - chains: None, metadata: None, amount: None, description, + chains: None, metadata: None, amount: None, description: String::new(), features: OfferFeatures::empty(), absolute_expiry: None, issuer: None, paths: None, supported_quantity: Quantity::One, signing_pubkey: Some(signing_pubkey), }, @@ -255,7 +256,7 @@ macro_rules! offer_derived_metadata_builder_methods { ($secp_context: ty) => { /// [`InvoiceRequest::verify`]: crate::offers::invoice_request::InvoiceRequest::verify /// [`ExpandedKey`]: crate::ln::inbound_payment::ExpandedKey pub fn deriving_signing_pubkey( - description: String, node_id: PublicKey, expanded_key: &ExpandedKey, entropy_source: ES, + node_id: PublicKey, expanded_key: &ExpandedKey, entropy_source: ES, secp_ctx: &'a Secp256k1<$secp_context> ) -> Self where ES::Target: EntropySource { let nonce = Nonce::from_entropy_source(entropy_source); @@ -263,7 +264,7 @@ macro_rules! offer_derived_metadata_builder_methods { ($secp_context: ty) => { let metadata = Metadata::DerivedSigningPubkey(derivation_material); Self { offer: OfferContents { - chains: None, metadata: Some(metadata), amount: None, description, + chains: None, metadata: Some(metadata), amount: None, description: String::new(), features: OfferFeatures::empty(), absolute_expiry: None, issuer: None, paths: None, supported_quantity: Quantity::One, signing_pubkey: Some(node_id), }, @@ -325,6 +326,14 @@ macro_rules! offer_builder_methods { ( $return_value } + /// Sets the [`Offer::description`]. + /// + /// Successive calls to this method will override the previous setting. + pub fn description($($self_mut)* $self: $self_type, description: String) -> $return_type { + $self.offer.description = description; + $return_value + } + /// Sets the [`Offer::issuer`]. /// /// Successive calls to this method will override the previous setting. @@ -1147,7 +1156,7 @@ mod tests { #[test] fn builds_offer_with_defaults() { - let offer = OfferBuilder::new("foo".into(), pubkey(42)).build().unwrap(); + let offer = OfferBuilder::new(pubkey(42)).build().unwrap(); let mut buffer = Vec::new(); offer.write(&mut buffer).unwrap(); @@ -1157,7 +1166,7 @@ mod tests { assert!(offer.supports_chain(ChainHash::using_genesis_block(Network::Bitcoin))); assert_eq!(offer.metadata(), None); assert_eq!(offer.amount(), None); - assert_eq!(offer.description(), PrintableString("foo")); + assert_eq!(offer.description(), PrintableString("")); assert_eq!(offer.offer_features(), &OfferFeatures::empty()); assert_eq!(offer.absolute_expiry(), None); #[cfg(feature = "std")] @@ -1174,7 +1183,7 @@ mod tests { metadata: None, currency: None, amount: None, - description: Some(&String::from("foo")), + description: Some(&String::from("")), features: None, absolute_expiry: None, paths: None, @@ -1194,7 +1203,7 @@ mod tests { let mainnet = ChainHash::using_genesis_block(Network::Bitcoin); let testnet = ChainHash::using_genesis_block(Network::Testnet); - let offer = OfferBuilder::new("foo".into(), pubkey(42)) + let offer = OfferBuilder::new(pubkey(42)) .chain(Network::Bitcoin) .build() .unwrap(); @@ -1202,7 +1211,7 @@ mod tests { assert_eq!(offer.chains(), vec![mainnet]); assert_eq!(offer.as_tlv_stream().chains, None); - let offer = OfferBuilder::new("foo".into(), pubkey(42)) + let offer = OfferBuilder::new(pubkey(42)) .chain(Network::Testnet) .build() .unwrap(); @@ -1210,7 +1219,7 @@ mod tests { assert_eq!(offer.chains(), vec![testnet]); assert_eq!(offer.as_tlv_stream().chains, Some(&vec![testnet])); - let offer = OfferBuilder::new("foo".into(), pubkey(42)) + let offer = OfferBuilder::new(pubkey(42)) .chain(Network::Testnet) .chain(Network::Testnet) .build() @@ -1219,7 +1228,7 @@ mod tests { assert_eq!(offer.chains(), vec![testnet]); assert_eq!(offer.as_tlv_stream().chains, Some(&vec![testnet])); - let offer = OfferBuilder::new("foo".into(), pubkey(42)) + let offer = OfferBuilder::new(pubkey(42)) .chain(Network::Bitcoin) .chain(Network::Testnet) .build() @@ -1232,14 +1241,14 @@ mod tests { #[test] fn builds_offer_with_metadata() { - let offer = OfferBuilder::new("foo".into(), pubkey(42)) + let offer = OfferBuilder::new(pubkey(42)) .metadata(vec![42; 32]).unwrap() .build() .unwrap(); assert_eq!(offer.metadata(), Some(&vec![42; 32])); assert_eq!(offer.as_tlv_stream().metadata, Some(&vec![42; 32])); - let offer = OfferBuilder::new("foo".into(), pubkey(42)) + let offer = OfferBuilder::new(pubkey(42)) .metadata(vec![42; 32]).unwrap() .metadata(vec![43; 32]).unwrap() .build() @@ -1250,7 +1259,6 @@ mod tests { #[test] fn builds_offer_with_metadata_derived() { - let desc = "foo".to_string(); let node_id = recipient_pubkey(); let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32])); let entropy = FixedEntropy {}; @@ -1259,7 +1267,7 @@ mod tests { #[cfg(c_bindings)] use super::OfferWithDerivedMetadataBuilder as OfferBuilder; let offer = OfferBuilder - ::deriving_signing_pubkey(desc, node_id, &expanded_key, &entropy, &secp_ctx) + ::deriving_signing_pubkey(node_id, &expanded_key, &entropy, &secp_ctx) .amount_msats(1000) .build().unwrap(); assert_eq!(offer.signing_pubkey(), Some(node_id)); @@ -1302,7 +1310,6 @@ mod tests { #[test] fn builds_offer_with_derived_signing_pubkey() { - let desc = "foo".to_string(); let node_id = recipient_pubkey(); let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32])); let entropy = FixedEntropy {}; @@ -1320,7 +1327,7 @@ mod tests { #[cfg(c_bindings)] use super::OfferWithDerivedMetadataBuilder as OfferBuilder; let offer = OfferBuilder - ::deriving_signing_pubkey(desc, node_id, &expanded_key, &entropy, &secp_ctx) + ::deriving_signing_pubkey(node_id, &expanded_key, &entropy, &secp_ctx) .amount_msats(1000) .path(blinded_path) .build().unwrap(); @@ -1367,7 +1374,7 @@ mod tests { let bitcoin_amount = Amount::Bitcoin { amount_msats: 1000 }; let currency_amount = Amount::Currency { iso4217_code: *b"USD", amount: 10 }; - let offer = OfferBuilder::new("foo".into(), pubkey(42)) + let offer = OfferBuilder::new(pubkey(42)) .amount_msats(1000) .build() .unwrap(); @@ -1377,10 +1384,10 @@ mod tests { assert_eq!(tlv_stream.currency, None); #[cfg(not(c_bindings))] - let builder = OfferBuilder::new("foo".into(), pubkey(42)) + let builder = OfferBuilder::new(pubkey(42)) .amount(currency_amount.clone()); #[cfg(c_bindings)] - let mut builder = OfferBuilder::new("foo".into(), pubkey(42)); + let mut builder = OfferBuilder::new(pubkey(42)); #[cfg(c_bindings)] builder.amount(currency_amount.clone()); let tlv_stream = builder.offer.as_tlv_stream(); @@ -1392,7 +1399,7 @@ mod tests { Err(e) => assert_eq!(e, Bolt12SemanticError::UnsupportedCurrency), } - let offer = OfferBuilder::new("foo".into(), pubkey(42)) + let offer = OfferBuilder::new(pubkey(42)) .amount(currency_amount.clone()) .amount(bitcoin_amount.clone()) .build() @@ -1402,22 +1409,40 @@ mod tests { assert_eq!(tlv_stream.currency, None); let invalid_amount = Amount::Bitcoin { amount_msats: MAX_VALUE_MSAT + 1 }; - match OfferBuilder::new("foo".into(), pubkey(42)).amount(invalid_amount).build() { + match OfferBuilder::new(pubkey(42)).amount(invalid_amount).build() { Ok(_) => panic!("expected error"), Err(e) => assert_eq!(e, Bolt12SemanticError::InvalidAmount), } } + #[test] + fn builds_offer_with_description() { + let offer = OfferBuilder::new(pubkey(42)) + .description("foo".into()) + .build() + .unwrap(); + assert_eq!(offer.description(), PrintableString("foo")); + assert_eq!(offer.as_tlv_stream().description, Some(&String::from("foo"))); + + let offer = OfferBuilder::new(pubkey(42)) + .description("foo".into()) + .description("bar".into()) + .build() + .unwrap(); + assert_eq!(offer.description(), PrintableString("bar")); + assert_eq!(offer.as_tlv_stream().description, Some(&String::from("bar"))); + } + #[test] fn builds_offer_with_features() { - let offer = OfferBuilder::new("foo".into(), pubkey(42)) + let offer = OfferBuilder::new(pubkey(42)) .features_unchecked(OfferFeatures::unknown()) .build() .unwrap(); assert_eq!(offer.offer_features(), &OfferFeatures::unknown()); assert_eq!(offer.as_tlv_stream().features, Some(&OfferFeatures::unknown())); - let offer = OfferBuilder::new("foo".into(), pubkey(42)) + let offer = OfferBuilder::new(pubkey(42)) .features_unchecked(OfferFeatures::unknown()) .features_unchecked(OfferFeatures::empty()) .build() @@ -1432,7 +1457,7 @@ mod tests { let past_expiry = Duration::from_secs(0); let now = future_expiry - Duration::from_secs(1_000); - let offer = OfferBuilder::new("foo".into(), pubkey(42)) + let offer = OfferBuilder::new(pubkey(42)) .absolute_expiry(future_expiry) .build() .unwrap(); @@ -1442,7 +1467,7 @@ mod tests { assert_eq!(offer.absolute_expiry(), Some(future_expiry)); assert_eq!(offer.as_tlv_stream().absolute_expiry, Some(future_expiry.as_secs())); - let offer = OfferBuilder::new("foo".into(), pubkey(42)) + let offer = OfferBuilder::new(pubkey(42)) .absolute_expiry(future_expiry) .absolute_expiry(past_expiry) .build() @@ -1475,7 +1500,7 @@ mod tests { }, ]; - let offer = OfferBuilder::new("foo".into(), pubkey(42)) + let offer = OfferBuilder::new(pubkey(42)) .path(paths[0].clone()) .path(paths[1].clone()) .build() @@ -1490,20 +1515,20 @@ mod tests { #[test] fn builds_offer_with_issuer() { - let offer = OfferBuilder::new("foo".into(), pubkey(42)) - .issuer("bar".into()) + let offer = OfferBuilder::new(pubkey(42)) + .issuer("foo".into()) .build() .unwrap(); - assert_eq!(offer.issuer(), Some(PrintableString("bar"))); - assert_eq!(offer.as_tlv_stream().issuer, Some(&String::from("bar"))); + assert_eq!(offer.issuer(), Some(PrintableString("foo"))); + assert_eq!(offer.as_tlv_stream().issuer, Some(&String::from("foo"))); - let offer = OfferBuilder::new("foo".into(), pubkey(42)) + let offer = OfferBuilder::new(pubkey(42)) + .issuer("foo".into()) .issuer("bar".into()) - .issuer("baz".into()) .build() .unwrap(); - assert_eq!(offer.issuer(), Some(PrintableString("baz"))); - assert_eq!(offer.as_tlv_stream().issuer, Some(&String::from("baz"))); + assert_eq!(offer.issuer(), Some(PrintableString("bar"))); + assert_eq!(offer.as_tlv_stream().issuer, Some(&String::from("bar"))); } #[test] @@ -1511,7 +1536,7 @@ mod tests { let one = NonZeroU64::new(1).unwrap(); let ten = NonZeroU64::new(10).unwrap(); - let offer = OfferBuilder::new("foo".into(), pubkey(42)) + let offer = OfferBuilder::new(pubkey(42)) .supported_quantity(Quantity::One) .build() .unwrap(); @@ -1519,7 +1544,7 @@ mod tests { assert_eq!(offer.supported_quantity(), Quantity::One); assert_eq!(tlv_stream.quantity_max, None); - let offer = OfferBuilder::new("foo".into(), pubkey(42)) + let offer = OfferBuilder::new(pubkey(42)) .supported_quantity(Quantity::Unbounded) .build() .unwrap(); @@ -1527,7 +1552,7 @@ mod tests { assert_eq!(offer.supported_quantity(), Quantity::Unbounded); assert_eq!(tlv_stream.quantity_max, Some(0)); - let offer = OfferBuilder::new("foo".into(), pubkey(42)) + let offer = OfferBuilder::new(pubkey(42)) .supported_quantity(Quantity::Bounded(ten)) .build() .unwrap(); @@ -1535,7 +1560,7 @@ mod tests { assert_eq!(offer.supported_quantity(), Quantity::Bounded(ten)); assert_eq!(tlv_stream.quantity_max, Some(10)); - let offer = OfferBuilder::new("foo".into(), pubkey(42)) + let offer = OfferBuilder::new(pubkey(42)) .supported_quantity(Quantity::Bounded(one)) .build() .unwrap(); @@ -1543,7 +1568,7 @@ mod tests { assert_eq!(offer.supported_quantity(), Quantity::Bounded(one)); assert_eq!(tlv_stream.quantity_max, Some(1)); - let offer = OfferBuilder::new("foo".into(), pubkey(42)) + let offer = OfferBuilder::new(pubkey(42)) .supported_quantity(Quantity::Bounded(ten)) .supported_quantity(Quantity::One) .build() @@ -1555,7 +1580,7 @@ mod tests { #[test] fn fails_requesting_invoice_with_unknown_required_features() { - match OfferBuilder::new("foo".into(), pubkey(42)) + match OfferBuilder::new(pubkey(42)) .features_unchecked(OfferFeatures::unknown()) .build().unwrap() .request_invoice(vec![1; 32], pubkey(43)) @@ -1567,7 +1592,7 @@ mod tests { #[test] fn parses_offer_with_chains() { - let offer = OfferBuilder::new("foo".into(), pubkey(42)) + let offer = OfferBuilder::new(pubkey(42)) .chain(Network::Bitcoin) .chain(Network::Testnet) .build() @@ -1579,7 +1604,7 @@ mod tests { #[test] fn parses_offer_with_amount() { - let offer = OfferBuilder::new("foo".into(), pubkey(42)) + let offer = OfferBuilder::new(pubkey(42)) .amount(Amount::Bitcoin { amount_msats: 1000 }) .build() .unwrap(); @@ -1625,7 +1650,7 @@ mod tests { #[test] fn parses_offer_with_description() { - let offer = OfferBuilder::new("foo".into(), pubkey(42)).build().unwrap(); + let offer = OfferBuilder::new(pubkey(42)).build().unwrap(); if let Err(e) = offer.to_string().parse::() { panic!("error parsing offer: {:?}", e); } @@ -1646,7 +1671,7 @@ mod tests { #[test] fn parses_offer_with_paths() { - let offer = OfferBuilder::new("foo".into(), pubkey(42)) + let offer = OfferBuilder::new(pubkey(42)) .path(BlindedPath { introduction_node: IntroductionNode::NodeId(pubkey(40)), blinding_point: pubkey(41), @@ -1669,7 +1694,7 @@ mod tests { panic!("error parsing offer: {:?}", e); } - let offer = OfferBuilder::new("foo".into(), pubkey(42)) + let offer = OfferBuilder::new(pubkey(42)) .path(BlindedPath { introduction_node: IntroductionNode::NodeId(pubkey(40)), blinding_point: pubkey(41), @@ -1685,7 +1710,7 @@ mod tests { panic!("error parsing offer: {:?}", e); } - let mut builder = OfferBuilder::new("foo".into(), pubkey(42)); + let mut builder = OfferBuilder::new(pubkey(42)); builder.offer.paths = Some(vec![]); let offer = builder.build().unwrap(); @@ -1699,7 +1724,7 @@ mod tests { #[test] fn parses_offer_with_quantity() { - let offer = OfferBuilder::new("foo".into(), pubkey(42)) + let offer = OfferBuilder::new(pubkey(42)) .supported_quantity(Quantity::One) .build() .unwrap(); @@ -1707,7 +1732,7 @@ mod tests { panic!("error parsing offer: {:?}", e); } - let offer = OfferBuilder::new("foo".into(), pubkey(42)) + let offer = OfferBuilder::new(pubkey(42)) .supported_quantity(Quantity::Unbounded) .build() .unwrap(); @@ -1715,7 +1740,7 @@ mod tests { panic!("error parsing offer: {:?}", e); } - let offer = OfferBuilder::new("foo".into(), pubkey(42)) + let offer = OfferBuilder::new(pubkey(42)) .supported_quantity(Quantity::Bounded(NonZeroU64::new(10).unwrap())) .build() .unwrap(); @@ -1723,7 +1748,7 @@ mod tests { panic!("error parsing offer: {:?}", e); } - let offer = OfferBuilder::new("foo".into(), pubkey(42)) + let offer = OfferBuilder::new(pubkey(42)) .supported_quantity(Quantity::Bounded(NonZeroU64::new(1).unwrap())) .build() .unwrap(); @@ -1734,7 +1759,7 @@ mod tests { #[test] fn parses_offer_with_node_id() { - let offer = OfferBuilder::new("foo".into(), pubkey(42)).build().unwrap(); + let offer = OfferBuilder::new(pubkey(42)).build().unwrap(); if let Err(e) = offer.to_string().parse::() { panic!("error parsing offer: {:?}", e); } @@ -1755,7 +1780,7 @@ mod tests { #[test] fn fails_parsing_offer_with_extra_tlv_records() { - let offer = OfferBuilder::new("foo".into(), pubkey(42)).build().unwrap(); + let offer = OfferBuilder::new(pubkey(42)).build().unwrap(); let mut encoded_offer = Vec::new(); offer.write(&mut encoded_offer).unwrap(); From db7e69667303fb4ba8c2ae6e792d09442260d7ad Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Wed, 24 Apr 2024 13:59:01 -0500 Subject: [PATCH 6/7] Don't require Refund::description in API Refunds currently require a description, though this may change to be optional. Remove the description requirement from the API, setting and empty string by default. --- lightning/src/ln/channelmanager.rs | 10 ++-- lightning/src/ln/offers_tests.rs | 30 ++++------- lightning/src/offers/invoice.rs | 12 ++--- lightning/src/offers/refund.rs | 81 ++++++++++++++++-------------- 4 files changed, 64 insertions(+), 69 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index e05a3ba46be..c9f431b0ec3 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -1658,13 +1658,13 @@ where /// let payment_id = PaymentId([42; 32]); /// let refund = channel_manager /// .create_refund_builder( -/// "coffee".to_string(), amount_msats, absolute_expiry, payment_id, retry, -/// max_total_routing_fee_msat +/// amount_msats, absolute_expiry, payment_id, retry, max_total_routing_fee_msat /// )? /// # ; /// # // Needed for compiling for c_bindings /// # let builder: lightning::offers::refund::RefundBuilder<_> = refund.into(); /// # let refund = builder +/// .description("coffee".to_string()) /// .payer_note("refund for order 1234".to_string()) /// .build()?; /// let bech32_refund = refund.to_string(); @@ -8621,8 +8621,8 @@ macro_rules! create_refund_builder { ($self: ident, $builder: ty) => { /// [`Bolt12Invoice::payment_paths`]: crate::offers::invoice::Bolt12Invoice::payment_paths /// [Avoiding Duplicate Payments]: #avoiding-duplicate-payments pub fn create_refund_builder( - &$self, description: String, amount_msats: u64, absolute_expiry: Duration, - payment_id: PaymentId, retry_strategy: Retry, max_total_routing_fee_msat: Option + &$self, amount_msats: u64, absolute_expiry: Duration, payment_id: PaymentId, + retry_strategy: Retry, max_total_routing_fee_msat: Option ) -> Result<$builder, Bolt12SemanticError> { let node_id = $self.get_our_node_id(); let expanded_key = &$self.inbound_payment_key; @@ -8631,7 +8631,7 @@ macro_rules! create_refund_builder { ($self: ident, $builder: ty) => { let path = $self.create_blinded_path().map_err(|_| Bolt12SemanticError::MissingPaths)?; let builder = RefundBuilder::deriving_payer_id( - description, node_id, expanded_key, entropy, secp_ctx, amount_msats, payment_id + node_id, expanded_key, entropy, secp_ctx, amount_msats, payment_id )? .chain_hash($self.chain_hash) .absolute_expiry(absolute_expiry) diff --git a/lightning/src/ln/offers_tests.rs b/lightning/src/ln/offers_tests.rs index b36e9de39e4..24ff1c25c8f 100644 --- a/lightning/src/ln/offers_tests.rs +++ b/lightning/src/ln/offers_tests.rs @@ -484,9 +484,7 @@ fn creates_and_pays_for_refund_using_two_hop_blinded_path() { let absolute_expiry = Duration::from_secs(u64::MAX); let payment_id = PaymentId([1; 32]); let refund = david.node - .create_refund_builder( - "refund".to_string(), 10_000_000, absolute_expiry, payment_id, Retry::Attempts(0), None - ) + .create_refund_builder(10_000_000, absolute_expiry, payment_id, Retry::Attempts(0), None) .unwrap() .build().unwrap(); assert_eq!(refund.amount_msats(), 10_000_000); @@ -613,9 +611,7 @@ fn creates_and_pays_for_refund_using_one_hop_blinded_path() { let absolute_expiry = Duration::from_secs(u64::MAX); let payment_id = PaymentId([1; 32]); let refund = bob.node - .create_refund_builder( - "refund".to_string(), 10_000_000, absolute_expiry, payment_id, Retry::Attempts(0), None - ) + .create_refund_builder(10_000_000, absolute_expiry, payment_id, Retry::Attempts(0), None) .unwrap() .build().unwrap(); assert_eq!(refund.amount_msats(), 10_000_000); @@ -724,9 +720,7 @@ fn pays_for_refund_without_blinded_paths() { let absolute_expiry = Duration::from_secs(u64::MAX); let payment_id = PaymentId([1; 32]); let refund = bob.node - .create_refund_builder( - "refund".to_string(), 10_000_000, absolute_expiry, payment_id, Retry::Attempts(0), None - ) + .create_refund_builder(10_000_000, absolute_expiry, payment_id, Retry::Attempts(0), None) .unwrap() .clear_paths() .build().unwrap(); @@ -780,7 +774,7 @@ fn fails_creating_refund_without_blinded_paths() { let payment_id = PaymentId([1; 32]); match nodes[0].node.create_refund_builder( - "refund".to_string(), 10_000, absolute_expiry, payment_id, Retry::Attempts(0), None + 10_000, absolute_expiry, payment_id, Retry::Attempts(0), None ) { Ok(_) => panic!("Expected error"), Err(e) => assert_eq!(e, Bolt12SemanticError::MissingPaths), @@ -831,9 +825,7 @@ fn fails_sending_invoice_with_unsupported_chain_for_refund() { let absolute_expiry = Duration::from_secs(u64::MAX); let payment_id = PaymentId([1; 32]); let refund = bob.node - .create_refund_builder( - "refund".to_string(), 10_000_000, absolute_expiry, payment_id, Retry::Attempts(0), None - ) + .create_refund_builder(10_000_000, absolute_expiry, payment_id, Retry::Attempts(0), None) .unwrap() .chain(Network::Signet) .build().unwrap(); @@ -932,13 +924,13 @@ fn fails_creating_refund_with_duplicate_payment_id() { let payment_id = PaymentId([1; 32]); assert!( nodes[0].node.create_refund_builder( - "refund".to_string(), 10_000, absolute_expiry, payment_id, Retry::Attempts(0), None + 10_000, absolute_expiry, payment_id, Retry::Attempts(0), None ).is_ok() ); expect_recent_payment!(nodes[0], RecentPaymentDetails::AwaitingInvoice, payment_id); match nodes[0].node.create_refund_builder( - "refund".to_string(), 10_000, absolute_expiry, payment_id, Retry::Attempts(0), None + 10_000, absolute_expiry, payment_id, Retry::Attempts(0), None ) { Ok(_) => panic!("Expected error"), Err(e) => assert_eq!(e, Bolt12SemanticError::DuplicatePaymentId), @@ -1049,9 +1041,7 @@ fn fails_sending_invoice_without_blinded_payment_paths_for_refund() { let absolute_expiry = Duration::from_secs(u64::MAX); let payment_id = PaymentId([1; 32]); let refund = david.node - .create_refund_builder( - "refund".to_string(), 10_000_000, absolute_expiry, payment_id, Retry::Attempts(0), None - ) + .create_refund_builder(10_000_000, absolute_expiry, payment_id, Retry::Attempts(0), None) .unwrap() .build().unwrap(); @@ -1100,9 +1090,7 @@ fn fails_paying_invoice_more_than_once() { let absolute_expiry = Duration::from_secs(u64::MAX); let payment_id = PaymentId([1; 32]); let refund = david.node - .create_refund_builder( - "refund".to_string(), 10_000_000, absolute_expiry, payment_id, Retry::Attempts(0), None - ) + .create_refund_builder(10_000_000, absolute_expiry, payment_id, Retry::Attempts(0), None) .unwrap() .build().unwrap(); expect_recent_payment!(david, RecentPaymentDetails::AwaitingInvoice, payment_id); diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index 647da4c4f3a..d2e2eecde04 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -1673,7 +1673,7 @@ mod tests { let payment_paths = payment_paths(); let payment_hash = payment_hash(); let now = now(); - let invoice = RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap() + let invoice = RefundBuilder::new(vec![1; 32], payer_pubkey(), 1000).unwrap() .build().unwrap() .respond_with_no_std(payment_paths.clone(), payment_hash, recipient_pubkey(), now) .unwrap() @@ -1688,7 +1688,7 @@ mod tests { assert_eq!(invoice.offer_chains(), None); assert_eq!(invoice.metadata(), None); assert_eq!(invoice.amount(), None); - assert_eq!(invoice.description(), PrintableString("foo")); + assert_eq!(invoice.description(), PrintableString("")); assert_eq!(invoice.offer_features(), None); assert_eq!(invoice.absolute_expiry(), None); assert_eq!(invoice.message_paths(), &[]); @@ -1724,7 +1724,7 @@ mod tests { metadata: None, currency: None, amount: None, - description: Some(&String::from("foo")), + description: Some(&String::from("")), features: None, absolute_expiry: None, paths: None, @@ -1803,7 +1803,7 @@ mod tests { let future_expiry = Duration::from_secs(u64::max_value()); let past_expiry = Duration::from_secs(0); - if let Err(e) = RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap() + if let Err(e) = RefundBuilder::new(vec![1; 32], payer_pubkey(), 1000).unwrap() .absolute_expiry(future_expiry) .build().unwrap() .respond_with(payment_paths(), payment_hash(), recipient_pubkey()) @@ -1813,7 +1813,7 @@ mod tests { panic!("error building invoice: {:?}", e); } - match RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap() + match RefundBuilder::new(vec![1; 32], payer_pubkey(), 1000).unwrap() .absolute_expiry(past_expiry) .build().unwrap() .respond_with(payment_paths(), payment_hash(), recipient_pubkey()) @@ -1887,7 +1887,7 @@ mod tests { let entropy = FixedEntropy {}; let secp_ctx = Secp256k1::new(); - let refund = RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap() + let refund = RefundBuilder::new(vec![1; 32], payer_pubkey(), 1000).unwrap() .build().unwrap(); if let Err(e) = refund diff --git a/lightning/src/offers/refund.rs b/lightning/src/offers/refund.rs index f9aa90ba050..bcbab0fd04b 100644 --- a/lightning/src/offers/refund.rs +++ b/lightning/src/offers/refund.rs @@ -48,7 +48,8 @@ //! let pubkey = PublicKey::from(keys); //! //! let expiration = SystemTime::now() + Duration::from_secs(24 * 60 * 60); -//! let refund = RefundBuilder::new("coffee, large".to_string(), vec![1; 32], pubkey, 20_000)? +//! let refund = RefundBuilder::new(vec![1; 32], pubkey, 20_000)? +//! .description("coffee, large".to_string()) //! .absolute_expiry(expiration.duration_since(SystemTime::UNIX_EPOCH).unwrap()) //! .issuer("Foo Bar".to_string()) //! .path(create_blinded_path()) @@ -149,8 +150,8 @@ macro_rules! refund_explicit_metadata_builder_methods { () => { /// Creates a new builder for a refund using the [`Refund::payer_id`] for the public node id to /// send to if no [`Refund::paths`] are set. Otherwise, it may be a transient pubkey. /// - /// Additionally, sets the required [`Refund::description`], [`Refund::payer_metadata`], and - /// [`Refund::amount_msats`]. + /// Additionally, sets the required (empty) [`Refund::description`], [`Refund::payer_metadata`], + /// and [`Refund::amount_msats`]. /// /// # Note /// @@ -160,7 +161,7 @@ macro_rules! refund_explicit_metadata_builder_methods { () => { /// [`ChannelManager`]: crate::ln::channelmanager::ChannelManager /// [`ChannelManager::create_refund_builder`]: crate::ln::channelmanager::ChannelManager::create_refund_builder pub fn new( - description: String, metadata: Vec, payer_id: PublicKey, amount_msats: u64 + metadata: Vec, payer_id: PublicKey, amount_msats: u64 ) -> Result { if amount_msats > MAX_VALUE_MSAT { return Err(Bolt12SemanticError::InvalidAmount); @@ -169,8 +170,8 @@ macro_rules! refund_explicit_metadata_builder_methods { () => { let metadata = Metadata::Bytes(metadata); Ok(Self { refund: RefundContents { - payer: PayerContents(metadata), description, absolute_expiry: None, issuer: None, - chain: None, amount_msats, features: InvoiceRequestFeatures::empty(), + payer: PayerContents(metadata), description: String::new(), absolute_expiry: None, + issuer: None, chain: None, amount_msats, features: InvoiceRequestFeatures::empty(), quantity: None, payer_id, payer_note: None, paths: None, }, secp_ctx: None, @@ -195,7 +196,7 @@ macro_rules! refund_builder_methods { ( /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest /// [`ExpandedKey`]: crate::ln::inbound_payment::ExpandedKey pub fn deriving_payer_id( - description: String, node_id: PublicKey, expanded_key: &ExpandedKey, entropy_source: ES, + node_id: PublicKey, expanded_key: &ExpandedKey, entropy_source: ES, secp_ctx: &'a Secp256k1<$secp_context>, amount_msats: u64, payment_id: PaymentId ) -> Result where ES::Target: EntropySource { if amount_msats > MAX_VALUE_MSAT { @@ -208,14 +209,22 @@ macro_rules! refund_builder_methods { ( let metadata = Metadata::DerivedSigningPubkey(derivation_material); Ok(Self { refund: RefundContents { - payer: PayerContents(metadata), description, absolute_expiry: None, issuer: None, - chain: None, amount_msats, features: InvoiceRequestFeatures::empty(), + payer: PayerContents(metadata), description: String::new(), absolute_expiry: None, + issuer: None, chain: None, amount_msats, features: InvoiceRequestFeatures::empty(), quantity: None, payer_id: node_id, payer_note: None, paths: None, }, secp_ctx: Some(secp_ctx), }) } + /// Sets the [`Refund::description`]. + /// + /// Successive calls to this method will override the previous setting. + pub fn description($($self_mut)* $self: $self_type, description: String) -> $return_type { + $self.refund.description = description; + $return_value + } + /// Sets the [`Refund::absolute_expiry`] as seconds since the Unix epoch. Any expiry that has /// already passed is valid and can be checked for using [`Refund::is_expired`]. /// @@ -944,7 +953,7 @@ mod tests { #[test] fn builds_refund_with_defaults() { - let refund = RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap() + let refund = RefundBuilder::new(vec![1; 32], payer_pubkey(), 1000).unwrap() .build().unwrap(); let mut buffer = Vec::new(); @@ -952,7 +961,7 @@ mod tests { assert_eq!(refund.bytes, buffer.as_slice()); assert_eq!(refund.payer_metadata(), &[1; 32]); - assert_eq!(refund.description(), PrintableString("foo")); + assert_eq!(refund.description(), PrintableString("")); assert_eq!(refund.absolute_expiry(), None); #[cfg(feature = "std")] assert!(!refund.is_expired()); @@ -973,7 +982,7 @@ mod tests { metadata: None, currency: None, amount: None, - description: Some(&String::from("foo")), + description: Some(&String::from("")), features: None, absolute_expiry: None, paths: None, @@ -1000,7 +1009,7 @@ mod tests { #[test] fn fails_building_refund_with_invalid_amount() { - match RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), MAX_VALUE_MSAT + 1) { + match RefundBuilder::new(vec![1; 32], payer_pubkey(), MAX_VALUE_MSAT + 1) { Ok(_) => panic!("expected error"), Err(e) => assert_eq!(e, Bolt12SemanticError::InvalidAmount), } @@ -1008,7 +1017,6 @@ mod tests { #[test] fn builds_refund_with_metadata_derived() { - let desc = "foo".to_string(); let node_id = payer_pubkey(); let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32])); let entropy = FixedEntropy {}; @@ -1016,7 +1024,7 @@ mod tests { let payment_id = PaymentId([1; 32]); let refund = RefundBuilder - ::deriving_payer_id(desc, node_id, &expanded_key, &entropy, &secp_ctx, 1000, payment_id) + ::deriving_payer_id(node_id, &expanded_key, &entropy, &secp_ctx, 1000, payment_id) .unwrap() .build().unwrap(); assert_eq!(refund.payer_id(), node_id); @@ -1063,7 +1071,6 @@ mod tests { #[test] fn builds_refund_with_derived_payer_id() { - let desc = "foo".to_string(); let node_id = payer_pubkey(); let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32])); let entropy = FixedEntropy {}; @@ -1080,7 +1087,7 @@ mod tests { }; let refund = RefundBuilder - ::deriving_payer_id(desc, node_id, &expanded_key, &entropy, &secp_ctx, 1000, payment_id) + ::deriving_payer_id(node_id, &expanded_key, &entropy, &secp_ctx, 1000, payment_id) .unwrap() .path(blinded_path) .build().unwrap(); @@ -1132,7 +1139,7 @@ mod tests { let past_expiry = Duration::from_secs(0); let now = future_expiry - Duration::from_secs(1_000); - let refund = RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap() + let refund = RefundBuilder::new(vec![1; 32], payer_pubkey(), 1000).unwrap() .absolute_expiry(future_expiry) .build() .unwrap(); @@ -1143,7 +1150,7 @@ mod tests { assert_eq!(refund.absolute_expiry(), Some(future_expiry)); assert_eq!(tlv_stream.absolute_expiry, Some(future_expiry.as_secs())); - let refund = RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap() + let refund = RefundBuilder::new(vec![1; 32], payer_pubkey(), 1000).unwrap() .absolute_expiry(future_expiry) .absolute_expiry(past_expiry) .build() @@ -1177,7 +1184,7 @@ mod tests { }, ]; - let refund = RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap() + let refund = RefundBuilder::new(vec![1; 32], payer_pubkey(), 1000).unwrap() .path(paths[0].clone()) .path(paths[1].clone()) .build() @@ -1192,7 +1199,7 @@ mod tests { #[test] fn builds_refund_with_issuer() { - let refund = RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap() + let refund = RefundBuilder::new(vec![1; 32], payer_pubkey(), 1000).unwrap() .issuer("bar".into()) .build() .unwrap(); @@ -1200,7 +1207,7 @@ mod tests { assert_eq!(refund.issuer(), Some(PrintableString("bar"))); assert_eq!(tlv_stream.issuer, Some(&String::from("bar"))); - let refund = RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap() + let refund = RefundBuilder::new(vec![1; 32], payer_pubkey(), 1000).unwrap() .issuer("bar".into()) .issuer("baz".into()) .build() @@ -1215,21 +1222,21 @@ mod tests { let mainnet = ChainHash::using_genesis_block(Network::Bitcoin); let testnet = ChainHash::using_genesis_block(Network::Testnet); - let refund = RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap() + let refund = RefundBuilder::new(vec![1; 32], payer_pubkey(), 1000).unwrap() .chain(Network::Bitcoin) .build().unwrap(); let (_, _, tlv_stream) = refund.as_tlv_stream(); assert_eq!(refund.chain(), mainnet); assert_eq!(tlv_stream.chain, None); - let refund = RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap() + let refund = RefundBuilder::new(vec![1; 32], payer_pubkey(), 1000).unwrap() .chain(Network::Testnet) .build().unwrap(); let (_, _, tlv_stream) = refund.as_tlv_stream(); assert_eq!(refund.chain(), testnet); assert_eq!(tlv_stream.chain, Some(&testnet)); - let refund = RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap() + let refund = RefundBuilder::new(vec![1; 32], payer_pubkey(), 1000).unwrap() .chain(Network::Regtest) .chain(Network::Testnet) .build().unwrap(); @@ -1240,14 +1247,14 @@ mod tests { #[test] fn builds_refund_with_quantity() { - let refund = RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap() + let refund = RefundBuilder::new(vec![1; 32], payer_pubkey(), 1000).unwrap() .quantity(10) .build().unwrap(); let (_, _, tlv_stream) = refund.as_tlv_stream(); assert_eq!(refund.quantity(), Some(10)); assert_eq!(tlv_stream.quantity, Some(10)); - let refund = RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap() + let refund = RefundBuilder::new(vec![1; 32], payer_pubkey(), 1000).unwrap() .quantity(10) .quantity(1) .build().unwrap(); @@ -1258,14 +1265,14 @@ mod tests { #[test] fn builds_refund_with_payer_note() { - let refund = RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap() + let refund = RefundBuilder::new(vec![1; 32], payer_pubkey(), 1000).unwrap() .payer_note("bar".into()) .build().unwrap(); let (_, _, tlv_stream) = refund.as_tlv_stream(); assert_eq!(refund.payer_note(), Some(PrintableString("bar"))); assert_eq!(tlv_stream.payer_note, Some(&String::from("bar"))); - let refund = RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap() + let refund = RefundBuilder::new(vec![1; 32], payer_pubkey(), 1000).unwrap() .payer_note("bar".into()) .payer_note("baz".into()) .build().unwrap(); @@ -1276,7 +1283,7 @@ mod tests { #[test] fn fails_responding_with_unknown_required_features() { - match RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap() + match RefundBuilder::new(vec![1; 32], payer_pubkey(), 1000).unwrap() .features_unchecked(InvoiceRequestFeatures::unknown()) .build().unwrap() .respond_with_no_std(payment_paths(), payment_hash(), recipient_pubkey(), now()) @@ -1288,7 +1295,7 @@ mod tests { #[test] fn parses_refund_with_metadata() { - let refund = RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap() + let refund = RefundBuilder::new(vec![1; 32], payer_pubkey(), 1000).unwrap() .build().unwrap(); if let Err(e) = refund.to_string().parse::() { panic!("error parsing refund: {:?}", e); @@ -1307,7 +1314,7 @@ mod tests { #[test] fn parses_refund_with_description() { - let refund = RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap() + let refund = RefundBuilder::new(vec![1; 32], payer_pubkey(), 1000).unwrap() .build().unwrap(); if let Err(e) = refund.to_string().parse::() { panic!("error parsing refund: {:?}", e); @@ -1326,7 +1333,7 @@ mod tests { #[test] fn parses_refund_with_amount() { - let refund = RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap() + let refund = RefundBuilder::new(vec![1; 32], payer_pubkey(), 1000).unwrap() .build().unwrap(); if let Err(e) = refund.to_string().parse::() { panic!("error parsing refund: {:?}", e); @@ -1355,7 +1362,7 @@ mod tests { #[test] fn parses_refund_with_payer_id() { - let refund = RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap() + let refund = RefundBuilder::new(vec![1; 32], payer_pubkey(), 1000).unwrap() .build().unwrap(); if let Err(e) = refund.to_string().parse::() { panic!("error parsing refund: {:?}", e); @@ -1394,7 +1401,7 @@ mod tests { }, ]; - let refund = RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap() + let refund = RefundBuilder::new(vec![1; 32], payer_pubkey(), 1000).unwrap() .absolute_expiry(past_expiry) .issuer("bar".into()) .path(paths[0].clone()) @@ -1423,7 +1430,7 @@ mod tests { #[test] fn fails_parsing_refund_with_unexpected_fields() { - let refund = RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap() + let refund = RefundBuilder::new(vec![1; 32], payer_pubkey(), 1000).unwrap() .build().unwrap(); if let Err(e) = refund.to_string().parse::() { panic!("error parsing refund: {:?}", e); @@ -1499,7 +1506,7 @@ mod tests { fn fails_parsing_refund_with_extra_tlv_records() { let secp_ctx = Secp256k1::new(); let keys = KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap()); - let refund = RefundBuilder::new("foo".into(), vec![1; 32], keys.public_key(), 1000).unwrap() + let refund = RefundBuilder::new(vec![1; 32], keys.public_key(), 1000).unwrap() .build().unwrap(); let mut encoded_refund = Vec::new(); From e61001f60daa4f3d96c3b3f54cd3a8b00051ebe0 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Thu, 2 May 2024 11:43:04 -0500 Subject: [PATCH 7/7] Only require description when offer has an amount The spec was changed to allow excluding an offer description if the offer doesn't have an amount. However, it is still required when the amount is set. --- lightning/src/offers/invoice.rs | 12 ++--- lightning/src/offers/invoice_request.rs | 5 ++- lightning/src/offers/offer.rs | 60 ++++++++++++++++--------- 3 files changed, 48 insertions(+), 29 deletions(-) diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index d2e2eecde04..47e05edf910 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -717,7 +717,7 @@ macro_rules! invoice_accessors { ($self: ident, $contents: expr) => { /// From [`Offer::description`] or [`Refund::description`]. /// /// [`Offer::description`]: crate::offers::offer::Offer::description - pub fn description(&$self) -> PrintableString { + pub fn description(&$self) -> Option { $contents.description() } @@ -952,12 +952,12 @@ impl InvoiceContents { } } - fn description(&self) -> PrintableString { + fn description(&self) -> Option { match self { InvoiceContents::ForOffer { invoice_request, .. } => { invoice_request.inner.offer.description() }, - InvoiceContents::ForRefund { refund, .. } => refund.description(), + InvoiceContents::ForRefund { refund, .. } => Some(refund.description()), } } @@ -1546,7 +1546,7 @@ mod tests { assert_eq!(unsigned_invoice.offer_chains(), Some(vec![ChainHash::using_genesis_block(Network::Bitcoin)])); assert_eq!(unsigned_invoice.metadata(), None); assert_eq!(unsigned_invoice.amount(), Some(&Amount::Bitcoin { amount_msats: 1000 })); - assert_eq!(unsigned_invoice.description(), PrintableString("")); + assert_eq!(unsigned_invoice.description(), Some(PrintableString(""))); assert_eq!(unsigned_invoice.offer_features(), Some(&OfferFeatures::empty())); assert_eq!(unsigned_invoice.absolute_expiry(), None); assert_eq!(unsigned_invoice.message_paths(), &[]); @@ -1590,7 +1590,7 @@ mod tests { assert_eq!(invoice.offer_chains(), Some(vec![ChainHash::using_genesis_block(Network::Bitcoin)])); assert_eq!(invoice.metadata(), None); assert_eq!(invoice.amount(), Some(&Amount::Bitcoin { amount_msats: 1000 })); - assert_eq!(invoice.description(), PrintableString("")); + assert_eq!(invoice.description(), Some(PrintableString(""))); assert_eq!(invoice.offer_features(), Some(&OfferFeatures::empty())); assert_eq!(invoice.absolute_expiry(), None); assert_eq!(invoice.message_paths(), &[]); @@ -1688,7 +1688,7 @@ mod tests { assert_eq!(invoice.offer_chains(), None); assert_eq!(invoice.metadata(), None); assert_eq!(invoice.amount(), None); - assert_eq!(invoice.description(), PrintableString("")); + assert_eq!(invoice.description(), Some(PrintableString(""))); assert_eq!(invoice.offer_features(), None); assert_eq!(invoice.absolute_expiry(), None); assert_eq!(invoice.message_paths(), &[]); diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index 90d6d5bf308..aa6d7c067d0 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -1263,7 +1263,7 @@ mod tests { assert_eq!(unsigned_invoice_request.chains(), vec![ChainHash::using_genesis_block(Network::Bitcoin)]); assert_eq!(unsigned_invoice_request.metadata(), None); assert_eq!(unsigned_invoice_request.amount(), Some(&Amount::Bitcoin { amount_msats: 1000 })); - assert_eq!(unsigned_invoice_request.description(), PrintableString("")); + assert_eq!(unsigned_invoice_request.description(), Some(PrintableString(""))); assert_eq!(unsigned_invoice_request.offer_features(), &OfferFeatures::empty()); assert_eq!(unsigned_invoice_request.absolute_expiry(), None); assert_eq!(unsigned_invoice_request.paths(), &[]); @@ -1295,7 +1295,7 @@ mod tests { assert_eq!(invoice_request.chains(), vec![ChainHash::using_genesis_block(Network::Bitcoin)]); assert_eq!(invoice_request.metadata(), None); assert_eq!(invoice_request.amount(), Some(&Amount::Bitcoin { amount_msats: 1000 })); - assert_eq!(invoice_request.description(), PrintableString("")); + assert_eq!(invoice_request.description(), Some(PrintableString(""))); assert_eq!(invoice_request.offer_features(), &OfferFeatures::empty()); assert_eq!(invoice_request.absolute_expiry(), None); assert_eq!(invoice_request.paths(), &[]); @@ -1996,6 +1996,7 @@ mod tests { } let invoice_request = OfferBuilder::new(recipient_pubkey()) + .description("foo".to_string()) .amount(Amount::Currency { iso4217_code: *b"USD", amount: 1000 }) .build_unchecked() .request_invoice(vec![1; 32], payer_pubkey()).unwrap() diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs index baac949515a..6df9b30a4ef 100644 --- a/lightning/src/offers/offer.rs +++ b/lightning/src/offers/offer.rs @@ -209,9 +209,8 @@ impl MetadataStrategy for DerivedMetadata {} macro_rules! offer_explicit_metadata_builder_methods { ( $self: ident, $self_type: ty, $return_type: ty, $return_value: expr ) => { - /// Creates a new builder for an offer setting an empty [`Offer::description`] and using the - /// [`Offer::signing_pubkey`] for signing invoices. The associated secret key must be remembered - /// while the offer is valid. + /// Creates a new builder for an offer using the [`Offer::signing_pubkey`] for signing invoices. + /// The associated secret key must be remembered while the offer is valid. /// /// Use a different pubkey per offer to avoid correlating offers. /// @@ -225,7 +224,7 @@ macro_rules! offer_explicit_metadata_builder_methods { ( pub fn new(signing_pubkey: PublicKey) -> Self { Self { offer: OfferContents { - chains: None, metadata: None, amount: None, description: String::new(), + chains: None, metadata: None, amount: None, description: None, features: OfferFeatures::empty(), absolute_expiry: None, issuer: None, paths: None, supported_quantity: Quantity::One, signing_pubkey: Some(signing_pubkey), }, @@ -264,7 +263,7 @@ macro_rules! offer_derived_metadata_builder_methods { ($secp_context: ty) => { let metadata = Metadata::DerivedSigningPubkey(derivation_material); Self { offer: OfferContents { - chains: None, metadata: Some(metadata), amount: None, description: String::new(), + chains: None, metadata: Some(metadata), amount: None, description: None, features: OfferFeatures::empty(), absolute_expiry: None, issuer: None, paths: None, supported_quantity: Quantity::One, signing_pubkey: Some(node_id), }, @@ -330,7 +329,7 @@ macro_rules! offer_builder_methods { ( /// /// Successive calls to this method will override the previous setting. pub fn description($($self_mut)* $self: $self_type, description: String) -> $return_type { - $self.offer.description = description; + $self.offer.description = Some(description); $return_value } @@ -373,6 +372,10 @@ macro_rules! offer_builder_methods { ( None => {}, } + if $self.offer.amount.is_some() && $self.offer.description.is_none() { + $self.offer.description = Some(String::new()); + } + if let Some(chains) = &$self.offer.chains { if chains.len() == 1 && chains[0] == $self.offer.implied_chain() { $self.offer.chains = None; @@ -551,7 +554,7 @@ pub(super) struct OfferContents { chains: Option>, metadata: Option, amount: Option, - description: String, + description: Option, features: OfferFeatures, absolute_expiry: Option, issuer: Option, @@ -585,7 +588,7 @@ macro_rules! offer_accessors { ($self: ident, $contents: expr) => { /// A complete description of the purpose of the payment. Intended to be displayed to the user /// but with the caveat that it has not been verified in any way. - pub fn description(&$self) -> $crate::util::string::PrintableString { + pub fn description(&$self) -> Option<$crate::util::string::PrintableString> { $contents.description() } @@ -809,8 +812,8 @@ impl OfferContents { self.amount.as_ref() } - pub fn description(&self) -> PrintableString { - PrintableString(&self.description) + pub fn description(&self) -> Option { + self.description.as_ref().map(|description| PrintableString(description)) } pub fn features(&self) -> &OfferFeatures { @@ -954,7 +957,7 @@ impl OfferContents { metadata: self.metadata(), currency, amount, - description: Some(&self.description), + description: self.description.as_ref(), features, absolute_expiry: self.absolute_expiry.map(|duration| duration.as_secs()), paths: self.paths.as_ref(), @@ -1092,10 +1095,9 @@ impl TryFrom for OfferContents { (Some(iso4217_code), Some(amount)) => Some(Amount::Currency { iso4217_code, amount }), }; - let description = match description { - None => return Err(Bolt12SemanticError::MissingDescription), - Some(description) => description, - }; + if amount.is_some() && description.is_none() { + return Err(Bolt12SemanticError::MissingDescription); + } let features = features.unwrap_or_else(OfferFeatures::empty); @@ -1166,7 +1168,7 @@ mod tests { assert!(offer.supports_chain(ChainHash::using_genesis_block(Network::Bitcoin))); assert_eq!(offer.metadata(), None); assert_eq!(offer.amount(), None); - assert_eq!(offer.description(), PrintableString("")); + assert_eq!(offer.description(), None); assert_eq!(offer.offer_features(), &OfferFeatures::empty()); assert_eq!(offer.absolute_expiry(), None); #[cfg(feature = "std")] @@ -1183,7 +1185,7 @@ mod tests { metadata: None, currency: None, amount: None, - description: Some(&String::from("")), + description: None, features: None, absolute_expiry: None, paths: None, @@ -1421,7 +1423,7 @@ mod tests { .description("foo".into()) .build() .unwrap(); - assert_eq!(offer.description(), PrintableString("foo")); + assert_eq!(offer.description(), Some(PrintableString("foo"))); assert_eq!(offer.as_tlv_stream().description, Some(&String::from("foo"))); let offer = OfferBuilder::new(pubkey(42)) @@ -1429,8 +1431,15 @@ mod tests { .description("bar".into()) .build() .unwrap(); - assert_eq!(offer.description(), PrintableString("bar")); + assert_eq!(offer.description(), Some(PrintableString("bar"))); assert_eq!(offer.as_tlv_stream().description, Some(&String::from("bar"))); + + let offer = OfferBuilder::new(pubkey(42)) + .amount_msats(1000) + .build() + .unwrap(); + assert_eq!(offer.description(), Some(PrintableString(""))); + assert_eq!(offer.as_tlv_stream().description, Some(&String::from(""))); } #[test] @@ -1655,6 +1664,14 @@ mod tests { panic!("error parsing offer: {:?}", e); } + let offer = OfferBuilder::new(pubkey(42)) + .description("foo".to_string()) + .amount_msats(1000) + .build().unwrap(); + if let Err(e) = offer.to_string().parse::() { + panic!("error parsing offer: {:?}", e); + } + let mut tlv_stream = offer.as_tlv_stream(); tlv_stream.description = None; @@ -1875,7 +1892,7 @@ mod bolt12_tests { // Malformed: empty assert_eq!( "lno1".parse::(), - Err(Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::MissingDescription)), + Err(Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::MissingSigningPubkey)), ); // Malformed: truncated at type @@ -2000,7 +2017,8 @@ mod bolt12_tests { // Missing offer_description assert_eq!( - "lno1zcss9mk8y3wkklfvevcrszlmu23kfrxh49px20665dqwmn4p72pksese".parse::(), + // TODO: Match the spec once it is updated. + "lno1pqpq86qkyypwa3eyt44h6txtxquqh7lz5djge4afgfjn7k4rgrkuag0jsd5xvxg".parse::(), Err(Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::MissingDescription)), );