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..75bbcab93f8 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( @@ -1435,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); } @@ -1446,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) )?; @@ -1464,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; @@ -1633,6 +1646,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))), @@ -1725,6 +1739,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))), @@ -2365,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 9157613fcd9..60d1e3c02a2 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -747,7 +747,27 @@ 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) + } + + #[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) } } } @@ -855,6 +875,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 ) @@ -961,6 +986,7 @@ impl InvoiceRequestContentsWithoutPayerId { quantity: self.quantity, payer_id: None, payer_note: self.payer_note.as_ref(), + paths: None, }; (payer, offer, invoice_request) @@ -993,6 +1019,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)), @@ -1000,6 +1028,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 = @@ -1082,7 +1112,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 { @@ -1109,6 +1141,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, @@ -1233,7 +1269,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 +1301,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()); @@ -1300,6 +1336,7 @@ mod tests { quantity: None, payer_id: Some(&payer_pubkey()), payer_note: None, + paths: None, }, SignatureTlvStreamRef { signature: Some(&invoice_request.signature()) }, ), @@ -2260,7 +2297,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..8e93a93f873 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()); } } @@ -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() @@ -542,7 +548,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 +610,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 +892,7 @@ impl OfferContents { } } - pub(super) fn signing_pubkey(&self) -> PublicKey { + pub(super) fn signing_pubkey(&self) -> Option { self.signing_pubkey } @@ -905,8 +911,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 +951,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(), } } } @@ -1089,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) => 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 { @@ -1154,7 +1165,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 +1262,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 +1324,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 +1482,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))); @@ -1658,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)); + }, } } 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]