diff --git a/fuzz/src/full_stack.rs b/fuzz/src/full_stack.rs index 3a5a549dbbc..5ffc59dcf1a 100644 --- a/fuzz/src/full_stack.rs +++ b/fuzz/src/full_stack.rs @@ -80,9 +80,9 @@ use bitcoin::secp256k1::{self, Message, PublicKey, Scalar, Secp256k1, SecretKey} use lightning::util::dyn_signer::DynSigner; -use std::collections::VecDeque; use std::cell::RefCell; use std::cmp; +use std::collections::VecDeque; use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering}; use std::sync::{Arc, Mutex}; diff --git a/fuzz/src/invoice_request_deser.rs b/fuzz/src/invoice_request_deser.rs index 96d8515f0b5..24ad76594a0 100644 --- a/fuzz/src/invoice_request_deser.rs +++ b/fuzz/src/invoice_request_deser.rs @@ -17,9 +17,11 @@ use lightning::blinded_path::payment::{ use lightning::ln::channelmanager::MIN_FINAL_CLTV_EXPIRY_DELTA; use lightning::ln::inbound_payment::ExpandedKey; use lightning::offers::invoice::UnsignedBolt12Invoice; -use lightning::offers::invoice_request::{InvoiceRequest, InvoiceRequestFields}; +use lightning::offers::invoice_request::{ + CurrencyConversion, InvoiceRequest, InvoiceRequestFields, +}; use lightning::offers::nonce::Nonce; -use lightning::offers::offer::OfferId; +use lightning::offers::offer::{CurrencyCode, OfferId}; use lightning::offers::parse::Bolt12SemanticError; use lightning::sign::EntropySource; use lightning::types::features::BlindedHopFeatures; @@ -79,6 +81,14 @@ fn privkey(byte: u8) -> SecretKey { SecretKey::from_slice(&[byte; 32]).unwrap() } +struct FuzzCurrencyConversion; + +impl CurrencyConversion for FuzzCurrencyConversion { + fn fiat_to_msats(&self, _iso4217_code: CurrencyCode) -> Result { + unreachable!() + } +} + fn build_response( invoice_request: &InvoiceRequest, secp_ctx: &Secp256k1, ) -> Result { @@ -145,7 +155,7 @@ fn build_response( .unwrap(); let payment_hash = PaymentHash([42; 32]); - invoice_request.respond_with(vec![payment_path], payment_hash)?.build() + invoice_request.respond_with(&FuzzCurrencyConversion, vec![payment_path], payment_hash)?.build() } pub fn invoice_request_deser_test(data: &[u8], out: Out) { diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index e4614351ee9..41972b077a4 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -93,13 +93,13 @@ use crate::ln::outbound_payment::{ use crate::ln::types::ChannelId; use crate::offers::async_receive_offer_cache::AsyncReceiveOfferCache; use crate::offers::flow::{InvreqResponseInstructions, OffersMessageFlow}; -use crate::offers::invoice::{ - Bolt12Invoice, DerivedSigningPubkey, InvoiceBuilder, DEFAULT_RELATIVE_EXPIRY, -}; +use crate::offers::invoice::{Bolt12Invoice, UnsignedBolt12Invoice}; use crate::offers::invoice_error::InvoiceError; -use crate::offers::invoice_request::InvoiceRequest; +use crate::offers::invoice_request::{ + DefaultCurrencyConversion, InvoiceRequest, InvoiceRequestVerifiedFromOffer, +}; use crate::offers::nonce::Nonce; -use crate::offers::offer::Offer; +use crate::offers::offer::{Amount, Offer}; use crate::offers::parse::Bolt12SemanticError; use crate::offers::refund::Refund; use crate::offers::signer; @@ -2524,6 +2524,9 @@ pub struct ChannelManager< fee_estimator: LowerBoundedFeeEstimator, chain_monitor: M, tx_broadcaster: T, + #[cfg(test)] + pub(super) router: R, + #[cfg(not(test))] router: R, #[cfg(test)] @@ -2747,6 +2750,9 @@ pub struct ChannelManager< pub(super) entropy_source: ES, #[cfg(not(test))] entropy_source: ES, + #[cfg(test)] + pub(super) node_signer: NS, + #[cfg(not(test))] node_signer: NS, #[cfg(test)] pub(super) signer_provider: SP, @@ -3737,7 +3743,7 @@ where let flow = OffersMessageFlow::new( ChainHash::using_genesis_block(params.network), params.best_block, our_network_pubkey, current_timestamp, expanded_inbound_key, - node_signer.get_receive_auth_key(), secp_ctx.clone(), message_router + node_signer.get_receive_auth_key(), secp_ctx.clone(), message_router, false, ); ChannelManager { @@ -5294,6 +5300,7 @@ where let features = self.bolt12_invoice_features(); let outbound_pmts_res = self.pending_outbound_payments.static_invoice_received( invoice, + &DefaultCurrencyConversion, payment_id, features, best_block_height, @@ -7422,7 +7429,7 @@ where }; let payment_purpose_context = PaymentContext::Bolt12Offer(Bolt12OfferContext { - offer_id: verified_invreq.offer_id, + offer_id: verified_invreq.offer_id(), invoice_request: verified_invreq.fields(), }); let from_parts_res = events::PaymentPurpose::from_parts( @@ -12178,6 +12185,13 @@ where let entropy = &*self.entropy_source; let nonce = Nonce::from_entropy_source(entropy); + // If the offer is for a specific currency, ensure the amount is provided. + if let Some(Amount::Currency { iso4217_code: _, amount: _ }) = offer.amount() { + if amount_msats.is_none() { + return Err(Bolt12SemanticError::MissingAmount); + } + } + let builder = self.flow.create_invoice_request_builder( offer, nonce, payment_id, )?; @@ -12239,27 +12253,29 @@ where ) -> Result { let secp_ctx = &self.secp_ctx; - let amount_msats = refund.amount_msats(); - let relative_expiry = DEFAULT_RELATIVE_EXPIRY.as_secs() as u32; - let _persistence_guard = PersistenceNotifierGuard::notify_on_drop(self); - match self.create_inbound_payment(Some(amount_msats), relative_expiry, None) { - Ok((payment_hash, payment_secret)) => { - let entropy = &*self.entropy_source; - let builder = self.flow.create_invoice_builder_from_refund( - &self.router, entropy, refund, payment_hash, - payment_secret, self.list_usable_channels() - )?; - - let invoice = builder.allow_mpp().build_and_sign(secp_ctx)?; + let entropy = &*self.entropy_source; + let builder = self.flow.create_invoice_builder_from_refund( + &self.router, entropy, refund, self.list_usable_channels(), + |amount_msats, relative_expiry| { + self.create_inbound_payment( + Some(amount_msats), + relative_expiry, + None + ).map_err(|()| Bolt12SemanticError::InvalidAmount) + } + )?; - self.flow.enqueue_invoice(invoice.clone(), refund, self.get_peers_for_blinded_path())?; + let invoice = builder.allow_mpp().build_and_sign(secp_ctx)?; - Ok(invoice) - }, - Err(()) => Err(Bolt12SemanticError::InvalidAmount), + if refund.paths().is_empty() { + self.flow.enqueue_invoice_using_node_id(invoice.clone(), refund.payer_signing_pubkey(), self.get_peers_for_blinded_path())?; + } else { + self.flow.enqueue_invoice_using_reply_paths(invoice.clone(), refund.paths(), self.get_peers_for_blinded_path())?; } + + Ok(invoice) } /// Pays for an [`Offer`] looked up using [BIP 353] Human Readable Names resolved by the DNS @@ -12493,7 +12509,7 @@ where now } - fn get_peers_for_blinded_path(&self) -> Vec { + pub(crate) fn get_peers_for_blinded_path(&self) -> Vec { let per_peer_state = self.per_peer_state.read().unwrap(); per_peer_state .iter() @@ -14315,7 +14331,7 @@ where None => return None, }; - let invoice_request = match self.flow.verify_invoice_request(invoice_request, context) { + let invoice_request = match self.flow.verify_invoice_request(invoice_request, context, responder.clone()) { Ok(InvreqResponseInstructions::SendInvoice(invoice_request)) => invoice_request, Ok(InvreqResponseInstructions::SendStaticInvoice { recipient_id: _recipient_id, invoice_id: _invoice_id @@ -14326,37 +14342,89 @@ where return None }, + Ok(InvreqResponseInstructions::AsynchronouslyHandleResponse) => return None, Err(_) => return None, }; - let amount_msats = match InvoiceBuilder::::amount_msats( - &invoice_request.inner - ) { - Ok(amount_msats) => amount_msats, - Err(error) => return Some((OffersMessage::InvoiceError(error.into()), responder.respond())), + let get_payment_info = |amount_msats, relative_expiry| { + self.create_inbound_payment( + Some(amount_msats), + relative_expiry, + None + ).map_err(|_| Bolt12SemanticError::InvalidAmount) }; - let relative_expiry = DEFAULT_RELATIVE_EXPIRY.as_secs() as u32; - let (payment_hash, payment_secret) = match self.create_inbound_payment( - Some(amount_msats), relative_expiry, None - ) { - Ok((payment_hash, payment_secret)) => (payment_hash, payment_secret), - Err(()) => { - let error = Bolt12SemanticError::InvalidAmount; - return Some((OffersMessage::InvoiceError(error.into()), responder.respond())); + let (result, context) = match invoice_request { + InvoiceRequestVerifiedFromOffer::DerivedKeys(request) => { + let result = self.flow.create_invoice_builder_from_invoice_request_with_keys( + &self.router, + &*self.entropy_source, + &DefaultCurrencyConversion, + &request, + self.list_usable_channels(), + get_payment_info, + ); + + match result { + Ok((builder, context)) => { + let res = builder + .build_and_sign(&self.secp_ctx) + .map_err(InvoiceError::from); + + (res, context) + }, + Err(error) => { + return Some(( + OffersMessage::InvoiceError(InvoiceError::from(error)), + responder.respond(), + )); + }, + } }, - }; + InvoiceRequestVerifiedFromOffer::ExplicitKeys(request) => { + let result = self.flow.create_invoice_builder_from_invoice_request_without_keys( + &self.router, + &*self.entropy_source, + &DefaultCurrencyConversion, + &request, + self.list_usable_channels(), + get_payment_info, + ); - let entropy = &*self.entropy_source; - let (response, context) = self.flow.create_response_for_invoice_request( - &self.node_signer, &self.router, entropy, invoice_request, amount_msats, - payment_hash, payment_secret, self.list_usable_channels() - ); + match result { + Ok((builder, context)) => { + let res = builder + .build() + .map_err(InvoiceError::from) + .and_then(|invoice| { + #[cfg(c_bindings)] + let mut invoice = invoice; + invoice + .sign(|invoice: &UnsignedBolt12Invoice| self.node_signer.sign_bolt12_invoice(invoice)) + .map_err(InvoiceError::from) + }); + (res, context) + }, + Err(error) => { + return Some(( + OffersMessage::InvoiceError(InvoiceError::from(error)), + responder.respond(), + )); + }, + } + } + }; - match context { - Some(context) => Some((response, responder.respond_with_reply_path(context))), - None => Some((response, responder.respond())) - } + Some(match result { + Ok(invoice) => ( + OffersMessage::Invoice(invoice), + responder.respond_with_reply_path(context), + ), + Err(error) => ( + OffersMessage::InvoiceError(error), + responder.respond(), + ), + }) }, OffersMessage::Invoice(invoice) => { let payment_id = match self.flow.verify_bolt12_invoice(&invoice, context.as_ref()) { @@ -16898,6 +16966,7 @@ where args.node_signer.get_receive_auth_key(), secp_ctx.clone(), args.message_router, + false, ) .with_async_payments_offers_cache(async_receive_offer_cache); diff --git a/lightning/src/ln/offers_tests.rs b/lightning/src/ln/offers_tests.rs index 6c56ecc4270..4fc150b677c 100644 --- a/lightning/src/ln/offers_tests.rs +++ b/lightning/src/ln/offers_tests.rs @@ -51,13 +51,14 @@ use crate::blinded_path::payment::{Bolt12OfferContext, Bolt12RefundContext, Paym use crate::blinded_path::message::OffersContext; use crate::events::{ClosureReason, Event, HTLCHandlingFailureType, PaidBolt12Invoice, PaymentFailureReason, PaymentPurpose}; use crate::ln::channelmanager::{Bolt12PaymentError, PaymentId, RecentPaymentDetails, RecipientOnionFields, Retry, self}; +use crate::offers::flow::FlowEvents; use crate::types::features::Bolt12InvoiceFeatures; use crate::ln::functional_test_utils::*; use crate::ln::msgs::{BaseMessageHandler, ChannelMessageHandler, Init, NodeAnnouncement, OnionMessage, OnionMessageHandler, RoutingMessageHandler, SocketAddress, UnsignedGossipMessage, UnsignedNodeAnnouncement}; use crate::ln::outbound_payment::IDEMPOTENCY_TIMEOUT_TICKS; use crate::offers::invoice::Bolt12Invoice; use crate::offers::invoice_error::InvoiceError; -use crate::offers::invoice_request::{InvoiceRequest, InvoiceRequestFields}; +use crate::offers::invoice_request::{DefaultCurrencyConversion, InvoiceRequest, InvoiceRequestFields, InvoiceRequestVerifiedFromOffer}; use crate::offers::nonce::Nonce; use crate::offers::parse::Bolt12SemanticError; use crate::onion_message::messenger::{Destination, MessageSendInstructions, NodeIdMessageRouter, NullMessageRouter, PeeledOnion}; @@ -797,6 +798,122 @@ fn creates_and_pays_for_offer_using_one_hop_blinded_path() { expect_recent_payment!(bob, RecentPaymentDetails::Fulfilled, payment_id); } +/// Checks that an offer can be paid through a one-hop blinded path and that ephemeral pubkeys are +/// used rather than exposing a node's pubkey. However, the node's pubkey is still used as the +/// introduction node of the blinded path. +#[test] +fn creates_and_manually_respond_to_ir_then_pays_for_offer_using_one_hop_blinded_path() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let mut node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + node_chanmgrs[0].flow.enable_events(); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 10_000_000, 1_000_000_000); + + let alice = &nodes[0]; + let alice_id = alice.node.get_our_node_id(); + let bob = &nodes[1]; + let bob_id = bob.node.get_our_node_id(); + + let offer = alice.node + .create_offer_builder().unwrap() + .amount_msats(10_000_000) + .build().unwrap(); + assert_ne!(offer.issuer_signing_pubkey(), Some(alice_id)); + assert!(!offer.paths().is_empty()); + for path in offer.paths() { + assert!(check_compact_path_introduction_node(&path, bob, alice_id)); + } + + let payment_id = PaymentId([1; 32]); + bob.node.pay_for_offer(&offer, None, None, None, payment_id, Retry::Attempts(0), RouteParametersConfig::default()).unwrap(); + expect_recent_payment!(bob, RecentPaymentDetails::AwaitingInvoice, payment_id); + + let onion_message = bob.onion_messenger.next_onion_message_for_peer(alice_id).unwrap(); + alice.onion_messenger.handle_onion_message(bob_id, &onion_message); + + let flow_events = alice.node.flow.release_pending_flow_events(); + assert_eq!(flow_events.len(), 1, "expected exactly one flow event"); + + let (invoice_request, reply_path) = match flow_events.into_iter().next().unwrap() { + FlowEvents::InvoiceRequestReceived { + invoice_request: InvoiceRequestVerifiedFromOffer::DerivedKeys(req), + reply_path + } => (req, reply_path), + _ => panic!("Unexpected flow event"), + }; + + let payment_context = PaymentContext::Bolt12Offer(Bolt12OfferContext { + offer_id: offer.id(), + invoice_request: InvoiceRequestFields { + payer_signing_pubkey: invoice_request.payer_signing_pubkey(), + quantity: None, + payer_note_truncated: None, + human_readable_name: None, + }, + }); + assert_eq!(invoice_request.amount_msats(), Some(10_000_000)); + assert_ne!(invoice_request.payer_signing_pubkey(), bob_id); + assert!(check_compact_path_introduction_node(&reply_path, alice, bob_id)); + + // Create response for invoice request manually. + let get_payment_info = |amount_msats, relative_expiry| { + alice + .node + .create_inbound_payment(Some(amount_msats), relative_expiry, None) + .map_err(|_| Bolt12SemanticError::InvalidAmount) + }; + + let router = &alice.node.router; + let entropy = &*alice.node.entropy_source; + + let (builder, _) = alice + .node + .flow + .create_invoice_builder_from_invoice_request_with_keys( + router, + entropy, + &DefaultCurrencyConversion {}, + &invoice_request, + alice.node.list_usable_channels(), + get_payment_info, + ) + .expect("failed to create builder with derived keys"); + + let invoice = builder + .build_and_sign(&alice.node.secp_ctx) + .expect("failed to build and sign invoice"); + + alice + .node + .flow + .enqueue_invoice_using_reply_paths( + invoice, + &[reply_path], + alice.node.get_peers_for_blinded_path(), + ) + .expect("failed to enqueue invoice"); + + let onion_message = alice.onion_messenger.next_onion_message_for_peer(bob_id).unwrap(); + bob.onion_messenger.handle_onion_message(alice_id, &onion_message); + + let (invoice, reply_path) = extract_invoice(bob, &onion_message); + assert_eq!(invoice.amount_msats(), 10_000_000); + assert_ne!(invoice.signing_pubkey(), alice_id); + assert!(!invoice.payment_paths().is_empty()); + for path in invoice.payment_paths() { + assert_eq!(path.introduction_node(), &IntroductionNode::NodeId(alice_id)); + } + assert!(check_compact_path_introduction_node(&reply_path, bob, alice_id)); + + route_bolt12_payment(bob, &[alice], &invoice); + expect_recent_payment!(bob, RecentPaymentDetails::Pending, payment_id); + + claim_bolt12_payment(bob, &[alice], payment_context, &invoice); + expect_recent_payment!(bob, RecentPaymentDetails::Fulfilled, payment_id); +} + /// Checks that a refund can be paid through a one-hop blinded path and that ephemeral pubkeys are /// used rather than exposing a node's pubkey. However, the node's pubkey is still used as the /// introduction node of the blinded path. @@ -2276,11 +2393,19 @@ fn fails_paying_invoice_with_unknown_required_features() { let secp_ctx = Secp256k1::new(); let created_at = alice.node.duration_since_epoch(); - let invoice = invoice_request - .verify_using_recipient_data(nonce, &expanded_key, &secp_ctx).unwrap() - .respond_using_derived_keys_no_std(payment_paths, payment_hash, created_at).unwrap() - .features_unchecked(Bolt12InvoiceFeatures::unknown()) - .build_and_sign(&secp_ctx).unwrap(); + let verified_invoice_request = invoice_request + .verify_using_recipient_data(nonce, &expanded_key, &secp_ctx).unwrap(); + + let invoice = match verified_invoice_request { + InvoiceRequestVerifiedFromOffer::DerivedKeys(request) => { + request.respond_using_derived_keys_no_std(&DefaultCurrencyConversion, payment_paths, payment_hash, created_at).unwrap() + .features_unchecked(Bolt12InvoiceFeatures::unknown()) + .build_and_sign(&secp_ctx).unwrap() + }, + InvoiceRequestVerifiedFromOffer::ExplicitKeys(_) => { + panic!("Expected invoice request with keys"); + }, + }; // Enqueue an onion message containing the new invoice. let instructions = MessageSendInstructions::WithoutReplyPath { diff --git a/lightning/src/ln/outbound_payment.rs b/lightning/src/ln/outbound_payment.rs index 476964db889..03e029e2360 100644 --- a/lightning/src/ln/outbound_payment.rs +++ b/lightning/src/ln/outbound_payment.rs @@ -21,6 +21,7 @@ use crate::ln::channelmanager::{EventCompletionAction, HTLCSource, PaymentId}; use crate::ln::onion_utils; use crate::ln::onion_utils::{DecodedOnionFailure, HTLCFailReason}; use crate::offers::invoice::{Bolt12Invoice, DerivedSigningPubkey, InvoiceBuilder}; +use crate::offers::invoice_request::CurrencyConversion; use crate::offers::invoice_request::InvoiceRequest; use crate::offers::nonce::Nonce; use crate::offers::static_invoice::StaticInvoice; @@ -1107,11 +1108,15 @@ impl OutboundPayments { } #[rustfmt::skip] - pub(super) fn static_invoice_received( - &self, invoice: &StaticInvoice, payment_id: PaymentId, features: Bolt12InvoiceFeatures, + pub(super) fn static_invoice_received( + &self, invoice: &StaticInvoice, currency_conversion: CC, payment_id: PaymentId, features: Bolt12InvoiceFeatures, best_block_height: u32, duration_since_epoch: Duration, entropy_source: ES, pending_events: &Mutex)>> - ) -> Result<(), Bolt12PaymentError> where ES::Target: EntropySource { + ) -> Result<(), Bolt12PaymentError> + where + ES::Target: EntropySource, + CC::Target: CurrencyConversion + { macro_rules! abandon_with_entry { ($payment: expr, $reason: expr) => { assert!( @@ -1148,7 +1153,7 @@ impl OutboundPayments { return Err(Bolt12PaymentError::SendingFailed(RetryableSendFailure::PaymentExpired)) } - let amount_msat = match InvoiceBuilder::::amount_msats(invreq) { + let amount_msat = match InvoiceBuilder::::amount_msats(invreq, currency_conversion) { Ok(amt) => amt, Err(_) => { // We check this during invoice request parsing, when constructing the invreq's @@ -3134,7 +3139,7 @@ mod tests { .build().unwrap() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id).unwrap() .build_and_sign().unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), created_at).unwrap() + .respond_with_no_conversion(payment_paths(), payment_hash(), created_at).unwrap() .build().unwrap() .sign(recipient_sign).unwrap(); @@ -3181,7 +3186,7 @@ mod tests { .build().unwrap() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id).unwrap() .build_and_sign().unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() + .respond_with_no_conversion(payment_paths(), payment_hash(), now()).unwrap() .build().unwrap() .sign(recipient_sign).unwrap(); @@ -3244,7 +3249,7 @@ mod tests { .build().unwrap() .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id).unwrap() .build_and_sign().unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() + .respond_with_no_conversion(payment_paths(), payment_hash(), now()).unwrap() .build().unwrap() .sign(recipient_sign).unwrap(); diff --git a/lightning/src/offers/flow.rs b/lightning/src/offers/flow.rs index b6eee429ca2..dca8ecf1c8c 100644 --- a/lightning/src/offers/flow.rs +++ b/lightning/src/offers/flow.rs @@ -37,16 +37,18 @@ use crate::ln::inbound_payment; use crate::offers::async_receive_offer_cache::AsyncReceiveOfferCache; use crate::offers::invoice::{ Bolt12Invoice, DerivedSigningPubkey, ExplicitSigningPubkey, InvoiceBuilder, - UnsignedBolt12Invoice, DEFAULT_RELATIVE_EXPIRY, + DEFAULT_RELATIVE_EXPIRY, }; use crate::offers::invoice_error::InvoiceError; use crate::offers::invoice_request::{ - InvoiceRequest, InvoiceRequestBuilder, VerifiedInvoiceRequest, + CurrencyConversion, InvoiceRequest, InvoiceRequestBuilder, InvoiceRequestVerifiedFromOffer, + VerifiedInvoiceRequest, }; use crate::offers::nonce::Nonce; use crate::offers::offer::{Amount, DerivedMetadata, Offer, OfferBuilder}; use crate::offers::parse::Bolt12SemanticError; use crate::offers::refund::{Refund, RefundBuilder}; +use crate::offers::static_invoice::{StaticInvoice, StaticInvoiceBuilder}; use crate::onion_message::async_payments::{ AsyncPaymentsMessage, HeldHtlcAvailable, OfferPaths, OfferPathsRequest, ServeStaticInvoice, StaticInvoicePersisted, @@ -57,9 +59,7 @@ use crate::onion_message::messenger::{ use crate::onion_message::offers::OffersMessage; use crate::onion_message::packet::OnionMessageContents; use crate::routing::router::Router; -use crate::sign::{EntropySource, NodeSigner, ReceiveAuthKey}; - -use crate::offers::static_invoice::{StaticInvoice, StaticInvoiceBuilder}; +use crate::sign::{EntropySource, ReceiveAuthKey}; use crate::sync::{Mutex, RwLock}; use crate::types::payment::{PaymentHash, PaymentSecret}; use crate::util::ser::Writeable; @@ -70,6 +70,32 @@ use { crate::onion_message::dns_resolution::{DNSResolverMessage, DNSSECQuery, OMNameResolver}, }; +/// Defines the events that can be optionally triggered when processing offers messages. +/// +/// Once generated, these events are stored in the [`OffersMessageFlow`], where they can be +/// manually inspected and responded to. +pub enum FlowEvents { + /// Notifies that an [`InvoiceRequest`] has been received. + /// + /// To respond to this message: + /// - Based on the variant of [`InvoiceRequestVerifiedFromOffer`], create the appropriate invoice builder: + /// - [`InvoiceRequestVerifiedFromOffer::DerivedKeys`] → use + /// [`OffersMessageFlow::create_invoice_builder_from_invoice_request_with_keys`] + /// - [`InvoiceRequestVerifiedFromOffer::ExplicitKeys`] → use + /// [`OffersMessageFlow::create_invoice_builder_from_invoice_request_without_keys`] + /// - After building the invoice, sign it and send it back using the provided reply path via + /// [`OffersMessageFlow::enqueue_invoice_using_reply_paths`]. + /// + /// If the invoice request is invalid, respond with an [`InvoiceError`] using + /// [`OffersMessageFlow::enqueue_invoice_error`]. + InvoiceRequestReceived { + /// The received, verified invoice request. + invoice_request: InvoiceRequestVerifiedFromOffer, + /// The reply path to use when responding to the invoice request. + reply_path: BlindedMessagePath, + }, +} + /// A BOLT12 offers code and flow utility provider, which facilitates /// BOLT12 builder generation and onion message handling. /// @@ -91,6 +117,8 @@ where secp_ctx: Secp256k1, message_router: MR, + pub(crate) enable_events: bool, + #[cfg(not(any(test, feature = "_test_utils")))] pending_offers_messages: Mutex>, #[cfg(any(test, feature = "_test_utils"))] @@ -103,6 +131,8 @@ where pub(crate) hrn_resolver: OMNameResolver, #[cfg(feature = "dnssec")] pending_dns_onion_messages: Mutex>, + + pending_flow_events: Mutex>, } impl OffersMessageFlow @@ -114,6 +144,7 @@ where chain_hash: ChainHash, best_block: BestBlock, our_network_pubkey: PublicKey, current_timestamp: u32, inbound_payment_key: inbound_payment::ExpandedKey, receive_auth_key: ReceiveAuthKey, secp_ctx: Secp256k1, message_router: MR, + enable_events: bool, ) -> Self { Self { chain_hash, @@ -128,6 +159,8 @@ where secp_ctx, message_router, + enable_events, + pending_offers_messages: Mutex::new(Vec::new()), pending_async_payments_messages: Mutex::new(Vec::new()), @@ -137,6 +170,8 @@ where pending_dns_onion_messages: Mutex::new(Vec::new()), async_receive_offer_cache: Mutex::new(AsyncReceiveOfferCache::new()), + + pending_flow_events: Mutex::new(Vec::new()), } } @@ -152,6 +187,18 @@ where self } + /// Enables [`FlowEvents`] for this flow. + /// + /// By default, events are not emitted when processing offers messages. Calling this method + /// sets the internal `enable_events` flag to `true`, allowing you to receive [`FlowEvents`] + /// such as [`FlowEvents::InvoiceRequestReceived`]. + /// + /// This is useful when you want to manually inspect, handle, or respond to incoming + /// offers messages rather than having them processed automatically. + pub fn enable_events(&mut self) { + self.enable_events = true; + } + /// Sets the [`BlindedMessagePath`]s that we will use as an async recipient to interactively build /// [`Offer`]s with a static invoice server, so the server can serve [`StaticInvoice`]s to payers /// on our behalf when we're offline. @@ -393,7 +440,7 @@ fn enqueue_onion_message_with_reply_paths( pub enum InvreqResponseInstructions { /// We are the recipient of this payment, and a [`Bolt12Invoice`] should be sent in response to /// the invoice request since it is now verified. - SendInvoice(VerifiedInvoiceRequest), + SendInvoice(InvoiceRequestVerifiedFromOffer), /// We are a static invoice server and should respond to this invoice request by retrieving the /// [`StaticInvoice`] corresponding to the `recipient_id` and `invoice_id` and calling /// `OffersMessageFlow::enqueue_static_invoice`. @@ -407,6 +454,8 @@ pub enum InvreqResponseInstructions { /// An identifier for the specific invoice being requested by the payer. invoice_id: u128, }, + /// We are recipient of this payment, and should handle the response asynchronously. + AsynchronouslyHandleResponse, } impl OffersMessageFlow @@ -414,6 +463,7 @@ where MR::Target: MessageRouter, { /// Verifies an [`InvoiceRequest`] using the provided [`OffersContext`] or the [`InvoiceRequest::metadata`]. + /// It also helps determine the response instructions, corresponding to the verified invoice request must be taken. /// /// - If an [`OffersContext::InvoiceRequest`] with a `nonce` is provided, verification is performed using recipient context data. /// - If no context is provided but the [`InvoiceRequest`] contains [`Offer`] metadata, verification is performed using that metadata. @@ -426,27 +476,26 @@ where /// - The verification process (via recipient context data or metadata) fails. pub fn verify_invoice_request( &self, invoice_request: InvoiceRequest, context: Option, + responder: Responder, ) -> Result { let secp_ctx = &self.secp_ctx; let expanded_key = &self.inbound_payment_key; + if let Some(OffersContext::StaticInvoiceRequested { + recipient_id, + invoice_id, + path_absolute_expiry, + }) = context + { + if path_absolute_expiry < self.duration_since_epoch() { + return Err(()); + } + return Ok(InvreqResponseInstructions::SendStaticInvoice { recipient_id, invoice_id }); + } + let nonce = match context { - None if invoice_request.metadata().is_some() => None, Some(OffersContext::InvoiceRequest { nonce }) => Some(nonce), - Some(OffersContext::StaticInvoiceRequested { - recipient_id, - invoice_id, - path_absolute_expiry, - }) => { - if path_absolute_expiry < self.duration_since_epoch() { - return Err(()); - } - - return Ok(InvreqResponseInstructions::SendStaticInvoice { - recipient_id, - invoice_id, - }); - }, + None if invoice_request.metadata().is_some() => None, _ => return Err(()), }; @@ -457,7 +506,16 @@ where None => invoice_request.verify_using_metadata(expanded_key, secp_ctx), }?; - Ok(InvreqResponseInstructions::SendInvoice(invoice_request)) + if self.enable_events { + self.pending_flow_events.lock().unwrap().push(FlowEvents::InvoiceRequestReceived { + invoice_request, + reply_path: responder.into_blinded_path(), + }); + + Ok(InvreqResponseInstructions::AsynchronouslyHandleResponse) + } else { + Ok(InvreqResponseInstructions::SendInvoice(invoice_request)) + } } /// Verifies a [`Bolt12Invoice`] using the provided [`OffersContext`] or the invoice's payer metadata, @@ -849,13 +907,14 @@ where /// /// Returns an error if the refund targets a different chain or if no valid /// blinded path can be constructed. - pub fn create_invoice_builder_from_refund<'a, ES: Deref, R: Deref>( - &'a self, router: &R, entropy_source: ES, refund: &'a Refund, payment_hash: PaymentHash, - payment_secret: PaymentSecret, usable_channels: Vec, + pub fn create_invoice_builder_from_refund<'a, ES: Deref, R: Deref, F>( + &'a self, router: &R, entropy_source: ES, refund: &'a Refund, + usable_channels: Vec, get_payment_info: F, ) -> Result, Bolt12SemanticError> where ES::Target: EntropySource, R::Target: Router, + F: Fn(u64, u32) -> Result<(PaymentHash, PaymentSecret), Bolt12SemanticError>, { if refund.chain() != self.chain_hash { return Err(Bolt12SemanticError::UnsupportedChain); @@ -867,6 +926,8 @@ where let amount_msats = refund.amount_msats(); let relative_expiry = DEFAULT_RELATIVE_EXPIRY.as_secs() as u32; + let (payment_hash, payment_secret) = get_payment_info(amount_msats, relative_expiry)?; + let payment_context = PaymentContext::Bolt12Refund(Bolt12RefundContext {}); let payment_paths = self .create_blinded_payment_paths( @@ -902,93 +963,157 @@ where Ok(builder.into()) } - /// Creates a response for the provided [`VerifiedInvoiceRequest`]. + /// Creates an [`InvoiceBuilder`] with [`DerivedSigningPubkey`] for the + /// provided [`VerifiedInvoiceRequest`]. + /// + /// Returns the invoice builder along with a [`MessageContext`] that can + /// later be used to respond to the counterparty. /// - /// A response can be either an [`OffersMessage::Invoice`] with additional [`MessageContext`], - /// or an [`OffersMessage::InvoiceError`], depending on the [`InvoiceRequest`]. + /// Use this method when you want to inspect or modify the [`InvoiceBuilder`] + /// before signing and generating the final [`Bolt12Invoice`]. /// - /// An [`OffersMessage::InvoiceError`] will be generated if: - /// - We fail to generate valid payment paths to include in the [`Bolt12Invoice`]. - /// - We fail to generate a valid signed [`Bolt12Invoice`] for the [`InvoiceRequest`]. - pub fn create_response_for_invoice_request( - &self, signer: &NS, router: &R, entropy_source: ES, - invoice_request: VerifiedInvoiceRequest, amount_msats: u64, payment_hash: PaymentHash, - payment_secret: PaymentSecret, usable_channels: Vec, - ) -> (OffersMessage, Option) + /// # Errors + /// + /// Returns a [`Bolt12SemanticError`] if: + /// - User call the function with [`VerifiedInvoiceRequest`]. + /// - Valid blinded payment paths could not be generated for the [`Bolt12Invoice`]. + /// - The [`InvoiceBuilder`] could not be created from the [`InvoiceRequest`]. + pub fn create_invoice_builder_from_invoice_request_with_keys< + 'a, + ES: Deref, + R: Deref, + CC: Deref, + F, + >( + &'a self, router: &R, entropy_source: ES, currency_conversion: CC, + invoice_request: &'a VerifiedInvoiceRequest, + usable_channels: Vec, get_payment_info: F, + ) -> Result<(InvoiceBuilder<'a, DerivedSigningPubkey>, MessageContext), Bolt12SemanticError> where ES::Target: EntropySource, - NS::Target: NodeSigner, R::Target: Router, + CC::Target: CurrencyConversion, + F: Fn(u64, u32) -> Result<(PaymentHash, PaymentSecret), Bolt12SemanticError>, { let entropy = &*entropy_source; - let secp_ctx = &self.secp_ctx; + let conversion = &*currency_conversion; + let relative_expiry = DEFAULT_RELATIVE_EXPIRY.as_secs() as u32; + let amount_msats = InvoiceBuilder::::amount_msats( + &invoice_request.inner, + conversion, + )?; + + let (payment_hash, payment_secret) = get_payment_info(amount_msats, relative_expiry)?; + + let context = PaymentContext::Bolt12Offer(Bolt12OfferContext { + offer_id: invoice_request.offer_id, + invoice_request: invoice_request.fields(), + }); + + let payment_paths = self + .create_blinded_payment_paths( + router, + entropy, + usable_channels, + Some(amount_msats), + payment_secret, + context, + relative_expiry, + ) + .map_err(|_| Bolt12SemanticError::MissingPaths)?; + + #[cfg(feature = "std")] + let builder = invoice_request.respond_using_derived_keys(conversion, payment_paths, payment_hash); + #[cfg(not(feature = "std"))] + let builder = invoice_request.respond_using_derived_keys_no_std( + conversion, + payment_paths, + payment_hash, + Duration::from_secs(self.highest_seen_timestamp.load(Ordering::Acquire) as u64), + ); + let builder = builder.map(|b| InvoiceBuilder::from(b).allow_mpp())?; + + let context = MessageContext::Offers(OffersContext::InboundPayment { payment_hash }); + + Ok((builder, context)) + } + + /// Creates an [`InvoiceBuilder`] with [`ExplicitSigningPubkey`] for the + /// provided [`VerifiedInvoiceRequest`]. + /// + /// Returns the invoice builder along with a [`MessageContext`] that can + /// later be used to respond to the counterparty. + /// + /// Use this method when you want to inspect or modify the [`InvoiceBuilder`] + /// before signing and generating the final [`Bolt12Invoice`]. + /// + /// # Errors + /// + /// Returns a [`Bolt12SemanticError`] if: + /// - User call the function with [`VerifiedInvoiceRequest`]. + /// - Valid blinded payment paths could not be generated for the [`Bolt12Invoice`]. + /// - The [`InvoiceBuilder`] could not be created from the [`InvoiceRequest`]. + pub fn create_invoice_builder_from_invoice_request_without_keys< + 'a, + ES: Deref, + R: Deref, + CC: Deref, + F, + >( + &'a self, router: &R, entropy_source: ES, currency_conversion: CC, + invoice_request: &'a VerifiedInvoiceRequest, + usable_channels: Vec, get_payment_info: F, + ) -> Result<(InvoiceBuilder<'a, ExplicitSigningPubkey>, MessageContext), Bolt12SemanticError> + where + ES::Target: EntropySource, + R::Target: Router, + CC::Target: CurrencyConversion, + F: Fn(u64, u32) -> Result<(PaymentHash, PaymentSecret), Bolt12SemanticError>, + { + let entropy = &*entropy_source; + let conversion = &*currency_conversion; let relative_expiry = DEFAULT_RELATIVE_EXPIRY.as_secs() as u32; + let amount_msats = InvoiceBuilder::::amount_msats( + &invoice_request.inner, + conversion, + )?; + + let (payment_hash, payment_secret) = get_payment_info(amount_msats, relative_expiry)?; + let context = PaymentContext::Bolt12Offer(Bolt12OfferContext { offer_id: invoice_request.offer_id, invoice_request: invoice_request.fields(), }); - let payment_paths = match self.create_blinded_payment_paths( - router, - entropy, - usable_channels, - Some(amount_msats), - payment_secret, - context, - relative_expiry, - ) { - Ok(paths) => paths, - Err(_) => { - let error = InvoiceError::from(Bolt12SemanticError::MissingPaths); - return (OffersMessage::InvoiceError(error.into()), None); - }, - }; + let payment_paths = self + .create_blinded_payment_paths( + router, + entropy, + usable_channels, + Some(amount_msats), + payment_secret, + context, + relative_expiry, + ) + .map_err(|_| Bolt12SemanticError::MissingPaths)?; + #[cfg(feature = "std")] + let builder = invoice_request.respond_with(conversion, payment_paths, payment_hash); #[cfg(not(feature = "std"))] - let created_at = Duration::from_secs(self.highest_seen_timestamp.load(Ordering::Acquire) as u64); + let builder = invoice_request.respond_with_no_std( + conversion, + payment_paths, + payment_hash, + Duration::from_secs(self.highest_seen_timestamp.load(Ordering::Acquire) as u64), + ); - let response = if invoice_request.keys.is_some() { - #[cfg(feature = "std")] - let builder = invoice_request.respond_using_derived_keys(payment_paths, payment_hash); - #[cfg(not(feature = "std"))] - let builder = invoice_request.respond_using_derived_keys_no_std( - payment_paths, - payment_hash, - created_at, - ); - builder - .map(InvoiceBuilder::::from) - .and_then(|builder| builder.allow_mpp().build_and_sign(secp_ctx)) - .map_err(InvoiceError::from) - } else { - #[cfg(feature = "std")] - let builder = invoice_request.respond_with(payment_paths, payment_hash); - #[cfg(not(feature = "std"))] - let builder = invoice_request.respond_with_no_std(payment_paths, payment_hash, created_at); - builder - .map(InvoiceBuilder::::from) - .and_then(|builder| builder.allow_mpp().build()) - .map_err(InvoiceError::from) - .and_then(|invoice| { - #[cfg(c_bindings)] - let mut invoice = invoice; - invoice - .sign(|invoice: &UnsignedBolt12Invoice| signer.sign_bolt12_invoice(invoice)) - .map_err(InvoiceError::from) - }) - }; + let builder = builder.map(|b| InvoiceBuilder::from(b).allow_mpp())?; - match response { - Ok(invoice) => { - let context = - MessageContext::Offers(OffersContext::InboundPayment { payment_hash }); + let context = MessageContext::Offers(OffersContext::InboundPayment { payment_hash }); - (OffersMessage::Invoice(invoice), Some(context)) - }, - Err(error) => (OffersMessage::InvoiceError(error.into()), None), - } + Ok((builder, context)) } /// Enqueues the created [`InvoiceRequest`] to be sent to the counterparty. @@ -1014,8 +1139,6 @@ where /// The user must provide a list of [`MessageForwardNode`] that will be used to generate /// valid reply paths for the counterparty to send back the corresponding [`Bolt12Invoice`] /// or [`InvoiceError`]. - /// - /// [`supports_onion_messages`]: crate::types::features::Features::supports_onion_messages pub fn enqueue_invoice_request( &self, invoice_request: InvoiceRequest, payment_id: PaymentId, nonce: Nonce, peers: Vec, @@ -1051,21 +1174,21 @@ where Ok(()) } - /// Enqueues the created [`Bolt12Invoice`] corresponding to a [`Refund`] to be sent - /// to the counterparty. + /// Enqueues the provided [`Bolt12Invoice`] to be sent directly to the specified + /// [`PublicKey`] `destination`. /// - /// # Peers + /// This method should be used when there are no available [`BlindedMessagePath`]s + /// for routing the [`Bolt12Invoice`] and the counterparty’s node ID is known. /// - /// The user must provide a list of [`MessageForwardNode`] that will be used to generate valid - /// reply paths for the counterparty to send back the corresponding [`InvoiceError`] if we fail - /// to create blinded reply paths + /// # Reply Path Requirement /// - /// [`supports_onion_messages`]: crate::types::features::Features::supports_onion_messages - pub fn enqueue_invoice( - &self, invoice: Bolt12Invoice, refund: &Refund, peers: Vec, + /// Reply paths are generated from the given `peers` to allow the counterparty to return + /// an [`InvoiceError`] in case they fail to process the invoice. If valid reply paths + /// cannot be constructed, this method returns a [`Bolt12SemanticError::MissingPaths`]. + pub fn enqueue_invoice_using_node_id( + &self, invoice: Bolt12Invoice, destination: PublicKey, peers: Vec, ) -> Result<(), Bolt12SemanticError> { let payment_hash = invoice.payment_hash(); - let context = MessageContext::Offers(OffersContext::InboundPayment { payment_hash }); let reply_paths = self @@ -1074,28 +1197,68 @@ where let mut pending_offers_messages = self.pending_offers_messages.lock().unwrap(); - if refund.paths().is_empty() { - for reply_path in reply_paths { - let instructions = MessageSendInstructions::WithSpecifiedReplyPath { - destination: Destination::Node(refund.payer_signing_pubkey()), - reply_path, - }; - let message = OffersMessage::Invoice(invoice.clone()); - pending_offers_messages.push((message, instructions)); - } - } else { - let message = OffersMessage::Invoice(invoice); - enqueue_onion_message_with_reply_paths( - message, - refund.paths(), - reply_paths, - &mut pending_offers_messages, - ); + for reply_path in reply_paths { + let instructions = MessageSendInstructions::WithSpecifiedReplyPath { + destination: Destination::Node(destination), + reply_path, + }; + let message = OffersMessage::Invoice(invoice.clone()); + pending_offers_messages.push((message, instructions)); } Ok(()) } + /// Similar to [`Self::enqueue_invoice_using_node_id`], but uses [`BlindedMessagePath`]s + /// for routing the [`Bolt12Invoice`] instead of a direct node ID. + /// + /// Useful when the counterparty expects to receive invoices through onion-routed paths + /// for privacy or anonymity. + /// + /// For reply path requirements see [`Self::enqueue_invoice_using_node_id`]. + pub fn enqueue_invoice_using_reply_paths( + &self, invoice: Bolt12Invoice, paths: &[BlindedMessagePath], peers: Vec, + ) -> Result<(), Bolt12SemanticError> { + let payment_hash = invoice.payment_hash(); + let context = MessageContext::Offers(OffersContext::InboundPayment { payment_hash }); + + let reply_paths = self + .create_blinded_paths(peers, context) + .map_err(|_| Bolt12SemanticError::MissingPaths)?; + + let mut pending_offers_messages = self.pending_offers_messages.lock().unwrap(); + + let message = OffersMessage::Invoice(invoice); + enqueue_onion_message_with_reply_paths( + message, + paths, + reply_paths, + &mut pending_offers_messages, + ); + + Ok(()) + } + + /// Enqueues an [`InvoiceError`] to be sent to the counterparty via a specified + /// [`BlindedMessagePath`]. + /// + /// Since this method returns the invoice error to the counterparty without + /// expecting back a response, we enqueue it without a reply path. + pub fn enqueue_invoice_error( + &self, invoice_error: InvoiceError, path: BlindedMessagePath, + ) -> Result<(), Bolt12SemanticError> { + let mut pending_offers_messages = self.pending_offers_messages.lock().unwrap(); + + let instructions = MessageSendInstructions::WithoutReplyPath { + destination: Destination::BlindedPath(path), + }; + + let message = OffersMessage::InvoiceError(invoice_error); + pending_offers_messages.push((message, instructions)); + + Ok(()) + } + /// Forwards a [`StaticInvoice`] over the provided [`Responder`] in response to an /// [`InvoiceRequest`] that we as a static invoice server received on behalf of an often-offline /// recipient. @@ -1126,7 +1289,6 @@ where /// reply paths for the recipient to send back the corresponding [`ReleaseHeldHtlc`] onion message. /// /// [`ReleaseHeldHtlc`]: crate::onion_message::async_payments::ReleaseHeldHtlc - /// [`supports_onion_messages`]: crate::types::features::Features::supports_onion_messages pub fn enqueue_held_htlc_available( &self, invoice: &StaticInvoice, payment_id: PaymentId, peers: Vec, ) -> Result<(), Bolt12SemanticError> { @@ -1158,8 +1320,6 @@ where /// The user must provide a list of [`MessageForwardNode`] that will be used to generate /// valid reply paths for the counterparty to send back the corresponding response for /// the [`DNSSECQuery`] message. - /// - /// [`supports_onion_messages`]: crate::types::features::Features::supports_onion_messages #[cfg(feature = "dnssec")] pub fn enqueue_dns_onion_message( &self, message: DNSSECQuery, context: DNSResolverContext, dns_resolvers: Vec, @@ -1186,6 +1346,11 @@ where Ok(()) } + /// Enqueues the generated [`FlowEvents`] to be processed. + pub fn enqueue_flow_event(&self, flow_event: FlowEvents) { + self.pending_flow_events.lock().unwrap().push(flow_event); + } + /// Gets the enqueued [`OffersMessage`] with their corresponding [`MessageSendInstructions`]. pub fn release_pending_offers_messages(&self) -> Vec<(OffersMessage, MessageSendInstructions)> { core::mem::take(&mut self.pending_offers_messages.lock().unwrap()) @@ -1198,6 +1363,11 @@ where core::mem::take(&mut self.pending_async_payments_messages.lock().unwrap()) } + /// Gets the enqueued [`FlowEvents`] to be processed. + pub fn release_pending_flow_events(&self) -> Vec { + core::mem::take(&mut self.pending_flow_events.lock().unwrap()) + } + /// Gets the enqueued [`DNSResolverMessage`] with their corresponding [`MessageSendInstructions`]. #[cfg(feature = "dnssec")] pub fn release_pending_dns_messages( diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index 9751f52b046..aebe7e617b4 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -24,7 +24,7 @@ //! use bitcoin::secp256k1::{Keypair, PublicKey, Secp256k1, SecretKey}; //! use core::convert::TryFrom; //! use lightning::offers::invoice::UnsignedBolt12Invoice; -//! use lightning::offers::invoice_request::InvoiceRequest; +//! use lightning::offers::invoice_request::{DefaultCurrencyConversion, InvoiceRequest}; //! use lightning::offers::refund::Refund; //! use lightning::util::ser::Writeable; //! @@ -50,13 +50,13 @@ #![cfg_attr( feature = "std", doc = " - .respond_with(payment_paths, payment_hash)? + .respond_with(&DefaultCurrencyConversion, payment_paths, payment_hash)? " )] #![cfg_attr( not(feature = "std"), doc = " - .respond_with_no_std(payment_paths, payment_hash, core::time::Duration::from_secs(0))? + .respond_with_no_conversion(payment_paths, payment_hash, core::time::Duration::from_secs(0))? " )] //! # ) @@ -125,10 +125,10 @@ use crate::ln::msgs::DecodeError; use crate::offers::invoice_macros::invoice_builder_methods_test_common; use crate::offers::invoice_macros::{invoice_accessors_common, invoice_builder_methods_common}; use crate::offers::invoice_request::{ - ExperimentalInvoiceRequestTlvStream, ExperimentalInvoiceRequestTlvStreamRef, InvoiceRequest, - InvoiceRequestContents, InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef, - EXPERIMENTAL_INVOICE_REQUEST_TYPES, INVOICE_REQUEST_PAYER_ID_TYPE, INVOICE_REQUEST_TYPES, - IV_BYTES as INVOICE_REQUEST_IV_BYTES, + CurrencyConversion, ExperimentalInvoiceRequestTlvStream, + ExperimentalInvoiceRequestTlvStreamRef, InvoiceRequest, InvoiceRequestContents, + InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef, EXPERIMENTAL_INVOICE_REQUEST_TYPES, + INVOICE_REQUEST_PAYER_ID_TYPE, INVOICE_REQUEST_TYPES, IV_BYTES as INVOICE_REQUEST_IV_BYTES, }; use crate::offers::merkle::{ self, SignError, SignFn, SignatureTlvStream, SignatureTlvStreamRef, TaggedHash, TlvStream, @@ -158,6 +158,7 @@ use bitcoin::secp256k1::schnorr::Signature; use bitcoin::secp256k1::{self, Keypair, PublicKey, Secp256k1}; use bitcoin::{Network, WitnessProgram, WitnessVersion}; use core::hash::{Hash, Hasher}; +use core::ops::Deref; use core::time::Duration; #[allow(unused_imports)] @@ -241,11 +242,15 @@ impl SigningPubkeyStrategy for DerivedSigningPubkey {} macro_rules! invoice_explicit_signing_pubkey_builder_methods { ($self: ident, $self_type: ty) => { #[cfg_attr(c_bindings, allow(dead_code))] - pub(super) fn for_offer( - invoice_request: &'a InvoiceRequest, payment_paths: Vec, - created_at: Duration, payment_hash: PaymentHash, signing_pubkey: PublicKey, - ) -> Result { - let amount_msats = Self::amount_msats(invoice_request)?; + pub(super) fn for_offer( + invoice_request: &'a InvoiceRequest, currency_conversion: CC, + payment_paths: Vec, created_at: Duration, + payment_hash: PaymentHash, signing_pubkey: PublicKey, + ) -> Result + where + CC::Target: CurrencyConversion, + { + let amount_msats = Self::amount_msats(invoice_request, currency_conversion)?; let contents = InvoiceContents::ForOffer { invoice_request: invoice_request.contents.clone(), fields: Self::fields( @@ -313,11 +318,15 @@ macro_rules! invoice_explicit_signing_pubkey_builder_methods { macro_rules! invoice_derived_signing_pubkey_builder_methods { ($self: ident, $self_type: ty) => { #[cfg_attr(c_bindings, allow(dead_code))] - pub(super) fn for_offer_using_keys( - invoice_request: &'a InvoiceRequest, payment_paths: Vec, - created_at: Duration, payment_hash: PaymentHash, keys: Keypair, - ) -> Result { - let amount_msats = Self::amount_msats(invoice_request)?; + pub(super) fn for_offer_using_keys( + invoice_request: &'a InvoiceRequest, currency_conversion: CC, + payment_paths: Vec, created_at: Duration, + payment_hash: PaymentHash, keys: Keypair, + ) -> Result + where + CC::Target: CurrencyConversion, + { + let amount_msats = Self::amount_msats(invoice_request, currency_conversion)?; let signing_pubkey = keys.public_key(); let contents = InvoiceContents::ForOffer { invoice_request: invoice_request.contents.clone(), @@ -393,19 +402,60 @@ macro_rules! invoice_builder_methods { ( $self: ident, $self_type: ty, $return_type: ty, $return_value: expr, $type_param: ty $(, $self_mut: tt)? ) => { - pub(crate) fn amount_msats( - invoice_request: &InvoiceRequest, - ) -> Result { - match invoice_request.contents.inner.amount_msats() { - Some(amount_msats) => Ok(amount_msats), - None => match invoice_request.contents.inner.offer.amount() { - Some(Amount::Bitcoin { amount_msats }) => amount_msats - .checked_mul(invoice_request.quantity().unwrap_or(1)) - .ok_or(Bolt12SemanticError::InvalidAmount), - Some(Amount::Currency { .. }) => Err(Bolt12SemanticError::UnsupportedCurrency), - None => Err(Bolt12SemanticError::MissingAmount), - }, + fn calculate_offer_amount_msats( + amount: Amount, currency_conversion: CC, + ) -> Result + where + CC::Target: CurrencyConversion, + { + match amount { + Amount::Bitcoin { amount_msats } => Ok(amount_msats), + Amount::Currency { iso4217_code, amount } => currency_conversion + .fiat_to_msats(iso4217_code)? + .checked_mul(amount) + .ok_or(Bolt12SemanticError::InvalidAmount), + } + } + + pub(crate) fn amount_msats( + invoice_request: &InvoiceRequest, currency_conversion: CC, + ) -> Result + where + CC::Target: CurrencyConversion, + { + let inner = &invoice_request.contents.inner; + let quantity = invoice_request.quantity().unwrap_or(1); + + // Compute the Offer-implied amount (if the Offer specifies one), + // converting from fiat if necessary and scaling by quantity. + let offer_msats_opt = inner + .offer + .amount() + .map(|amt| { + Self::calculate_offer_amount_msats(amt, currency_conversion).and_then( + |unit_msats| { + unit_msats + .checked_mul(quantity) + .ok_or(Bolt12SemanticError::InvalidAmount) + }, + ) + }) + .transpose()?; + + // Case 1: The InvoiceRequest provides an explicit amount. + // In this case we must enforce the Offer's minimum (if any): + // reject if the IR's amount is below the Offer-implied floor. + if let Some(ir_msats) = inner.amount_msats() { + if offer_msats_opt.map_or(false, |offer_msats| ir_msats < offer_msats) { + return Err(Bolt12SemanticError::InsufficientAmount); + } + return Ok(ir_msats); } + + // Case 2: The IR has no explicit amount. + // Fallback to the Offer-implied amount if available. + // If neither IR nor Offer specify an amount, this is invalid. + offer_msats_opt.ok_or(Bolt12SemanticError::MissingAmount) } #[cfg_attr(c_bindings, allow(dead_code))] @@ -1818,7 +1868,8 @@ mod tests { use crate::ln::inbound_payment::ExpandedKey; use crate::ln::msgs::DecodeError; use crate::offers::invoice_request::{ - ExperimentalInvoiceRequestTlvStreamRef, InvoiceRequestTlvStreamRef, + DefaultCurrencyConversion, ExperimentalInvoiceRequestTlvStreamRef, + InvoiceRequestTlvStreamRef, InvoiceRequestVerifiedFromOffer, }; use crate::offers::merkle::{self, SignError, SignatureTlvStreamRef, TaggedHash, TlvStream}; use crate::offers::nonce::Nonce; @@ -1876,7 +1927,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths.clone(), payment_hash, now) + .respond_with_no_conversion(payment_paths.clone(), payment_hash, now) .unwrap() .build() .unwrap(); @@ -2147,7 +2198,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with(payment_paths(), payment_hash()) + .respond_with(&DefaultCurrencyConversion, payment_paths(), payment_hash()) .unwrap() .build() { @@ -2162,7 +2213,7 @@ mod tests { .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .build_unchecked_and_sign() - .respond_with(payment_paths(), payment_hash()) + .respond_with(&DefaultCurrencyConversion, payment_paths(), payment_hash()) .unwrap() .build() { @@ -2235,15 +2286,30 @@ mod tests { .build_and_sign() .unwrap(); - if let Err(e) = invoice_request + let verified_request = invoice_request .clone() .verify_using_recipient_data(nonce, &expanded_key, &secp_ctx) - .unwrap() - .respond_using_derived_keys_no_std(payment_paths(), payment_hash(), now()) - .unwrap() - .build_and_sign(&secp_ctx) - { - panic!("error building invoice: {:?}", e); + .unwrap(); + + match verified_request { + InvoiceRequestVerifiedFromOffer::DerivedKeys(req) => { + let invoice = req + .respond_using_derived_keys_no_std( + &DefaultCurrencyConversion, + payment_paths(), + payment_hash(), + now(), + ) + .unwrap() + .build_and_sign(&secp_ctx); + + if let Err(e) = invoice { + panic!("error building invoice: {:?}", e); + } + }, + InvoiceRequestVerifiedFromOffer::ExplicitKeys(_) => { + panic!("expected invoice request with keys"); + }, } let expanded_key = ExpandedKey::new([41; 32]); @@ -2263,13 +2329,14 @@ mod tests { .build_and_sign() .unwrap(); - match invoice_request - .verify_using_metadata(&expanded_key, &secp_ctx) - .unwrap() - .respond_using_derived_keys_no_std(payment_paths(), payment_hash(), now()) - { - Ok(_) => panic!("expected error"), - Err(e) => assert_eq!(e, Bolt12SemanticError::InvalidMetadata), + let verified_request = + invoice_request.verify_using_metadata(&expanded_key, &secp_ctx).unwrap(); + + match verified_request { + InvoiceRequestVerifiedFromOffer::DerivedKeys(_) => { + panic!("expected invoice request without keys") + }, + InvoiceRequestVerifiedFromOffer::ExplicitKeys(_) => (), } } @@ -2356,7 +2423,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now) + .respond_with_no_conversion(payment_paths(), payment_hash(), now) .unwrap() .relative_expiry(one_hour.as_secs() as u32) .build() @@ -2377,7 +2444,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now - one_hour) + .respond_with_no_conversion(payment_paths(), payment_hash(), now - one_hour) .unwrap() .relative_expiry(one_hour.as_secs() as u32 - 1) .build() @@ -2409,7 +2476,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_conversion(payment_paths(), payment_hash(), now()) .unwrap() .build() .unwrap() @@ -2439,7 +2506,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_conversion(payment_paths(), payment_hash(), now()) .unwrap() .build() .unwrap() @@ -2459,7 +2526,7 @@ mod tests { .quantity(u64::max_value()) .unwrap() .build_unchecked_and_sign() - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_conversion(payment_paths(), payment_hash(), now()) { Ok(_) => panic!("expected error"), Err(e) => assert_eq!(e, Bolt12SemanticError::InvalidAmount), @@ -2487,7 +2554,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_conversion(payment_paths(), payment_hash(), now()) .unwrap() .fallback_v0_p2wsh(&script.wscript_hash()) .fallback_v0_p2wpkh(&pubkey.wpubkey_hash().unwrap()) @@ -2543,7 +2610,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_conversion(payment_paths(), payment_hash(), now()) .unwrap() .allow_mpp() .build() @@ -2571,7 +2638,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_conversion(payment_paths(), payment_hash(), now()) .unwrap() .build() .unwrap() @@ -2589,7 +2656,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_conversion(payment_paths(), payment_hash(), now()) .unwrap() .build() .unwrap() @@ -2616,7 +2683,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_conversion(payment_paths(), payment_hash(), now()) .unwrap() .build() .unwrap() @@ -2693,7 +2760,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_conversion(payment_paths(), payment_hash(), now()) .unwrap() .build() .unwrap() @@ -2737,7 +2804,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_conversion(payment_paths(), payment_hash(), now()) .unwrap() .relative_expiry(3600) .build() @@ -2770,7 +2837,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_conversion(payment_paths(), payment_hash(), now()) .unwrap() .build() .unwrap() @@ -2814,7 +2881,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_conversion(payment_paths(), payment_hash(), now()) .unwrap() .build() .unwrap() @@ -2856,7 +2923,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_conversion(payment_paths(), payment_hash(), now()) .unwrap() .allow_mpp() .build() @@ -2899,11 +2966,13 @@ mod tests { .build_and_sign() .unwrap(); #[cfg(not(c_bindings))] - let invoice_builder = - invoice_request.respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap(); + let invoice_builder = invoice_request + .respond_with_no_conversion(payment_paths(), payment_hash(), now()) + .unwrap(); #[cfg(c_bindings)] - let mut invoice_builder = - invoice_request.respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap(); + let mut invoice_builder = invoice_request + .respond_with_no_conversion(payment_paths(), payment_hash(), now()) + .unwrap(); let invoice_builder = invoice_builder .fallback_v0_p2wsh(&script.wscript_hash()) .fallback_v0_p2wpkh(&pubkey.wpubkey_hash().unwrap()) @@ -2962,7 +3031,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_conversion(payment_paths(), payment_hash(), now()) .unwrap() .build() .unwrap() @@ -3050,6 +3119,7 @@ mod tests { .build_and_sign() .unwrap() .respond_with_no_std_using_signing_pubkey( + &DefaultCurrencyConversion, payment_paths(), payment_hash(), now(), @@ -3080,6 +3150,7 @@ mod tests { .build_and_sign() .unwrap() .respond_with_no_std_using_signing_pubkey( + &DefaultCurrencyConversion, payment_paths(), payment_hash(), now(), @@ -3121,7 +3192,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_conversion(payment_paths(), payment_hash(), now()) .unwrap() .amount_msats_unchecked(2000) .build() @@ -3150,7 +3221,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_conversion(payment_paths(), payment_hash(), now()) .unwrap() .amount_msats_unchecked(2000) .build() @@ -3214,7 +3285,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_conversion(payment_paths(), payment_hash(), now()) .unwrap() .build() .unwrap() @@ -3247,7 +3318,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_conversion(payment_paths(), payment_hash(), now()) .unwrap() .build() .unwrap() @@ -3290,7 +3361,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_conversion(payment_paths(), payment_hash(), now()) .unwrap() .build() .unwrap(); @@ -3329,7 +3400,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_conversion(payment_paths(), payment_hash(), now()) .unwrap() .build() .unwrap(); @@ -3375,7 +3446,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_conversion(payment_paths(), payment_hash(), now()) .unwrap() .experimental_baz(42) .build() @@ -3401,7 +3472,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_conversion(payment_paths(), payment_hash(), now()) .unwrap() .build() .unwrap(); @@ -3442,7 +3513,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_conversion(payment_paths(), payment_hash(), now()) .unwrap() .build() .unwrap(); @@ -3480,7 +3551,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_conversion(payment_paths(), payment_hash(), now()) .unwrap() .build() .unwrap() @@ -3521,7 +3592,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_conversion(payment_paths(), payment_hash(), now()) .unwrap() .build() .unwrap() @@ -3556,7 +3627,7 @@ mod tests { .unwrap() .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_conversion(payment_paths(), payment_hash(), now()) .unwrap() .build() .unwrap() @@ -3604,7 +3675,7 @@ mod tests { .unwrap(); let invoice = invoice_request - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_conversion(payment_paths(), payment_hash(), now()) .unwrap() .build() .unwrap() @@ -3650,7 +3721,7 @@ mod tests { .unwrap(); let invoice = invoice_request - .respond_with_no_std(payment_paths, payment_hash(), now) + .respond_with_no_conversion(payment_paths, payment_hash(), now) .unwrap() .build() .unwrap() diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index 27f32bcc1d2..d951be3b787 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -71,13 +71,15 @@ use crate::io; use crate::ln::channelmanager::PaymentId; use crate::ln::inbound_payment::{ExpandedKey, IV_LEN}; use crate::ln::msgs::DecodeError; +use crate::offers::invoice::{DerivedSigningPubkey, ExplicitSigningPubkey, SigningPubkeyStrategy}; use crate::offers::merkle::{ self, SignError, SignFn, SignatureTlvStream, SignatureTlvStreamRef, TaggedHash, TlvStream, }; use crate::offers::nonce::Nonce; use crate::offers::offer::{ - Amount, ExperimentalOfferTlvStream, ExperimentalOfferTlvStreamRef, Offer, OfferContents, - OfferId, OfferTlvStream, OfferTlvStreamRef, EXPERIMENTAL_OFFER_TYPES, OFFER_TYPES, + Amount, CurrencyCode, ExperimentalOfferTlvStream, ExperimentalOfferTlvStreamRef, Offer, + OfferContents, OfferId, OfferTlvStream, OfferTlvStreamRef, EXPERIMENTAL_OFFER_TYPES, + OFFER_TYPES, }; use crate::offers::parse::{Bolt12ParseError, Bolt12SemanticError, ParsedMessage}; use crate::offers::payer::{PayerContents, PayerTlvStream, PayerTlvStreamRef}; @@ -96,7 +98,7 @@ use bitcoin::secp256k1::schnorr::Signature; use bitcoin::secp256k1::{self, Keypair, PublicKey, Secp256k1}; #[cfg(not(c_bindings))] -use crate::offers::invoice::{DerivedSigningPubkey, ExplicitSigningPubkey, InvoiceBuilder}; +use crate::offers::invoice::InvoiceBuilder; #[cfg(c_bindings)] use crate::offers::invoice::{ InvoiceWithDerivedSigningPubkeyBuilder, InvoiceWithExplicitSigningPubkeyBuilder, @@ -104,6 +106,7 @@ use crate::offers::invoice::{ #[allow(unused_imports)] use crate::prelude::*; +use core::ops::Deref; /// Tag for the hash function used when signing an [`InvoiceRequest`]'s merkle root. pub const SIGNATURE_TAG: &'static str = concat!("lightning", "invoice_request", "signature"); @@ -573,6 +576,34 @@ impl AsRef for UnsignedInvoiceRequest { } } +/// A trait for converting fiat currencies into millisatoshis (msats). +/// +/// Implementations must return the conversion rate in **msats per minor unit** of the currency, +/// where the minor unit is determined by its ISO-4217 exponent: +/// - USD (exponent 2) → per **cent** (0.01 USD), not per dollar. +/// - JPY (exponent 0) → per **yen**. +/// - KWD (exponent 3) → per **fils** (0.001 KWD). +/// +/// # Caution: +/// +/// Returning msats per major unit will be off by a factor of 10^exponent (e.g. 100× for USD). +/// +/// This convention ensures amounts remain precise and purely integer-based when parsing and +/// validating BOLT12 invoice requests. +pub trait CurrencyConversion { + /// Converts a fiat currency specified by its ISO-4217 code into **msats per minor unit**. + fn fiat_to_msats(&self, iso4217_code: CurrencyCode) -> Result; +} + +/// A default implementation of the `CurrencyConversion` trait that does not support any currency conversions. +pub struct DefaultCurrencyConversion; + +impl CurrencyConversion for DefaultCurrencyConversion { + fn fiat_to_msats(&self, _iso4217_code: CurrencyCode) -> Result { + Err(Bolt12SemanticError::UnsupportedCurrency) + } +} + /// An `InvoiceRequest` is a request for a [`Bolt12Invoice`] formulated from an [`Offer`]. /// /// An offer may provide choices such as quantity, amount, chain, features, etc. An invoice request @@ -592,18 +623,18 @@ pub struct InvoiceRequest { /// [`InvoiceRequest::verify_using_recipient_data`] and exposes different ways to respond depending /// on whether the signing keys were derived. #[derive(Clone, Debug)] -pub struct VerifiedInvoiceRequest { +pub struct VerifiedInvoiceRequest { /// The identifier of the [`Offer`] for which the [`InvoiceRequest`] was made. pub offer_id: OfferId, /// The verified request. pub(crate) inner: InvoiceRequest, - /// Keys used for signing a [`Bolt12Invoice`] if they can be derived. + /// Keys for signing a [`Bolt12Invoice`] for the request. /// #[cfg_attr( feature = "std", - doc = "If `Some`, must call [`respond_using_derived_keys`] when responding. Otherwise, call [`respond_with`]." + doc = "If `DerivedSigningPubkey`, must call [`respond_using_derived_keys`] when responding. Otherwise, call [`respond_with`]." )] #[cfg_attr(feature = "std", doc = "")] /// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice @@ -612,7 +643,47 @@ pub struct VerifiedInvoiceRequest { doc = "[`respond_using_derived_keys`]: Self::respond_using_derived_keys" )] #[cfg_attr(feature = "std", doc = "[`respond_with`]: Self::respond_with")] - pub keys: Option, + pub keys: S, +} + +/// Represents a [`VerifiedInvoiceRequest`], along with information about how the resulting +/// [`Bolt12Invoice`] should be signed. +/// +/// The signing strategy determines whether the signing keys are: +/// - Derived either from the originating [`Offer`]’s metadata or recipient_data, or +/// - Explicitly provided. +/// +/// This distinction is required to produce a valid, signed [`Bolt12Invoice`] from a verified request. +/// +/// For more on key derivation strategies, see: +/// [`InvoiceRequest::verify_using_metadata`] and [`InvoiceRequest::verify_using_recipient_data`]. +/// +/// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice +pub enum InvoiceRequestVerifiedFromOffer { + /// A verified invoice request that uses signing keys derived from the originating [`Offer`]’s metadata or recipient_data. + DerivedKeys(VerifiedInvoiceRequest), + /// A verified invoice request that requires explicitly provided signing keys to sign the resulting [`Bolt12Invoice`]. + /// + /// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice + ExplicitKeys(VerifiedInvoiceRequest), +} + +impl InvoiceRequestVerifiedFromOffer { + /// Returns a reference to the underlying `InvoiceRequest`. + pub(crate) fn inner(&self) -> &InvoiceRequest { + match self { + InvoiceRequestVerifiedFromOffer::DerivedKeys(req) => &req.inner, + InvoiceRequestVerifiedFromOffer::ExplicitKeys(req) => &req.inner, + } + } + + /// Returns the `OfferId` of the offer this invoice request is for. + pub fn offer_id(&self) -> OfferId { + match self { + InvoiceRequestVerifiedFromOffer::DerivedKeys(req) => req.offer_id, + InvoiceRequestVerifiedFromOffer::ExplicitKeys(req) => req.offer_id, + } + } } /// The contents of an [`InvoiceRequest`], which may be shared with an [`Bolt12Invoice`]. @@ -715,14 +786,17 @@ macro_rules! invoice_request_respond_with_explicit_signing_pubkey_methods { ( /// /// [`Duration`]: core::time::Duration #[cfg(feature = "std")] - pub fn respond_with( - &$self, payment_paths: Vec, payment_hash: PaymentHash - ) -> Result<$builder, Bolt12SemanticError> { + pub fn respond_with( + &$self, currency_conversion: CC, payment_paths: Vec, payment_hash: PaymentHash + ) -> Result<$builder, Bolt12SemanticError> + where + CC::Target: CurrencyConversion + { let created_at = std::time::SystemTime::now() .duration_since(std::time::SystemTime::UNIX_EPOCH) .expect("SystemTime::now() should come after SystemTime::UNIX_EPOCH"); - $contents.respond_with_no_std(payment_paths, payment_hash, created_at) + $contents.respond_with_no_std(currency_conversion, payment_paths, payment_hash, created_at) } /// Creates an [`InvoiceBuilder`] for the request with the given required fields. @@ -745,15 +819,18 @@ macro_rules! invoice_request_respond_with_explicit_signing_pubkey_methods { ( /// /// If the originating [`Offer`] was created using [`OfferBuilder::deriving_signing_pubkey`], /// then first use [`InvoiceRequest::verify_using_metadata`] or - /// [`InvoiceRequest::verify_using_recipient_data`] and then [`VerifiedInvoiceRequest`] methods + /// [`InvoiceRequest::verify_using_recipient_data`] and then [`InvoiceRequestVerifiedFromOffer`] methods /// instead. /// /// [`Bolt12Invoice::created_at`]: crate::offers::invoice::Bolt12Invoice::created_at /// [`OfferBuilder::deriving_signing_pubkey`]: crate::offers::offer::OfferBuilder::deriving_signing_pubkey - pub fn respond_with_no_std( - &$self, payment_paths: Vec, payment_hash: PaymentHash, + pub fn respond_with_no_std( + &$self, currency_conversion: CC, payment_paths: Vec, payment_hash: PaymentHash, created_at: core::time::Duration - ) -> Result<$builder, Bolt12SemanticError> { + ) -> Result<$builder, Bolt12SemanticError> + where + CC::Target: CurrencyConversion + { if $contents.invoice_request_features().requires_unknown_bits() { return Err(Bolt12SemanticError::UnknownRequiredFeatures); } @@ -763,22 +840,33 @@ macro_rules! invoice_request_respond_with_explicit_signing_pubkey_methods { ( None => return Err(Bolt12SemanticError::MissingIssuerSigningPubkey), }; - <$builder>::for_offer(&$contents, payment_paths, created_at, payment_hash, signing_pubkey) + <$builder>::for_offer(&$contents, currency_conversion, 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, payment_hash: PaymentHash, - created_at: core::time::Duration, signing_pubkey: PublicKey + pub(crate) fn respond_with_no_conversion( + &$self, payment_paths: Vec, payment_hash: PaymentHash, created_at: core::time::Duration ) -> Result<$builder, Bolt12SemanticError> { + $contents.respond_with_no_std(&DefaultCurrencyConversion, payment_paths, payment_hash, created_at) + } + + #[cfg(test)] + #[allow(dead_code)] + pub(super) fn respond_with_no_std_using_signing_pubkey( + &$self, currency_conversion: CC, payment_paths: Vec, payment_hash: PaymentHash, + created_at: core::time::Duration, signing_pubkey: PublicKey + ) -> Result<$builder, Bolt12SemanticError> + where + CC::Target: CurrencyConversion + { debug_assert!($contents.contents.inner.offer.issuer_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) + <$builder>::for_offer(&$contents, currency_conversion, payment_paths, created_at, payment_hash, signing_pubkey) } } } @@ -801,17 +889,30 @@ macro_rules! invoice_request_verify_method { secp_ctx: &Secp256k1, #[cfg(c_bindings)] secp_ctx: &Secp256k1, - ) -> Result { + ) -> Result { let (offer_id, keys) = $self.contents.inner.offer.verify_using_metadata(&$self.bytes, key, secp_ctx)?; - Ok(VerifiedInvoiceRequest { - offer_id, + let inner = { #[cfg(not(c_bindings))] - inner: $self, + { $self } #[cfg(c_bindings)] - inner: $self.clone(), - keys, - }) + { $self.clone() } + }; + + let verified = match keys { + None => InvoiceRequestVerifiedFromOffer::ExplicitKeys(VerifiedInvoiceRequest { + offer_id, + inner, + keys: ExplicitSigningPubkey {}, + }), + Some(keys) => InvoiceRequestVerifiedFromOffer::DerivedKeys(VerifiedInvoiceRequest { + offer_id, + inner, + keys: DerivedSigningPubkey(keys), + }), + }; + + Ok(verified) } /// Verifies that the request was for an offer created using the given key by checking a nonce @@ -831,18 +932,32 @@ macro_rules! invoice_request_verify_method { secp_ctx: &Secp256k1, #[cfg(c_bindings)] secp_ctx: &Secp256k1, - ) -> Result { + ) -> Result { let (offer_id, keys) = $self.contents.inner.offer.verify_using_recipient_data( &$self.bytes, nonce, key, secp_ctx )?; - Ok(VerifiedInvoiceRequest { - offer_id, + + let inner = { #[cfg(not(c_bindings))] - inner: $self, + { $self } #[cfg(c_bindings)] - inner: $self.clone(), - keys, - }) + { $self.clone() } + }; + + let verified = match keys { + None => InvoiceRequestVerifiedFromOffer::ExplicitKeys(VerifiedInvoiceRequest { + offer_id, + inner, + keys: ExplicitSigningPubkey {}, + }), + Some(keys) => InvoiceRequestVerifiedFromOffer::DerivedKeys(VerifiedInvoiceRequest { + offer_id, + inner, + keys: DerivedSigningPubkey(keys), + }), + }; + + Ok(verified) } }; } @@ -920,14 +1035,17 @@ macro_rules! invoice_request_respond_with_derived_signing_pubkey_methods { ( /// /// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice #[cfg(feature = "std")] - pub fn respond_using_derived_keys( - &$self, payment_paths: Vec, payment_hash: PaymentHash - ) -> Result<$builder, Bolt12SemanticError> { + pub fn respond_using_derived_keys( + &$self, currency_conversion: CC, payment_paths: Vec, payment_hash: PaymentHash + ) -> Result<$builder, Bolt12SemanticError> + where + CC::Target: CurrencyConversion + { let created_at = std::time::SystemTime::now() .duration_since(std::time::SystemTime::UNIX_EPOCH) .expect("SystemTime::now() should come after SystemTime::UNIX_EPOCH"); - $self.respond_using_derived_keys_no_std(payment_paths, payment_hash, created_at) + $self.respond_using_derived_keys_no_std(currency_conversion, payment_paths, payment_hash, created_at) } /// Creates an [`InvoiceBuilder`] for the request using the given required fields and that uses @@ -937,18 +1055,18 @@ macro_rules! invoice_request_respond_with_derived_signing_pubkey_methods { ( /// See [`InvoiceRequest::respond_with_no_std`] for further details. /// /// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice - pub fn respond_using_derived_keys_no_std( - &$self, payment_paths: Vec, payment_hash: PaymentHash, + pub fn respond_using_derived_keys_no_std( + &$self, currency_conversion: CC, payment_paths: Vec, payment_hash: PaymentHash, created_at: core::time::Duration - ) -> Result<$builder, Bolt12SemanticError> { + ) -> Result<$builder, Bolt12SemanticError> + where + CC::Target: CurrencyConversion + { if $self.inner.invoice_request_features().requires_unknown_bits() { return Err(Bolt12SemanticError::UnknownRequiredFeatures); } - let keys = match $self.keys { - None => return Err(Bolt12SemanticError::InvalidMetadata), - Some(keys) => keys, - }; + let keys = $self.keys.0; match $contents.contents.inner.offer.issuer_signing_pubkey() { Some(signing_pubkey) => debug_assert_eq!(signing_pubkey, keys.public_key()), @@ -956,63 +1074,86 @@ macro_rules! invoice_request_respond_with_derived_signing_pubkey_methods { ( } <$builder>::for_offer_using_keys( - &$self.inner, payment_paths, created_at, payment_hash, keys + &$self.inner, currency_conversion, payment_paths, created_at, payment_hash, keys ) } } } -impl VerifiedInvoiceRequest { +macro_rules! fields_accessor { + ($self:ident, $inner:expr) => { + /// Fetch the [`InvoiceRequestFields`] for this verified invoice. + /// + /// These are fields which we expect to be useful when receiving a payment for this invoice + /// request, and include the returned [`InvoiceRequestFields`] in the + /// [`PaymentContext::Bolt12Offer`]. + /// + /// [`PaymentContext::Bolt12Offer`]: crate::blinded_path::payment::PaymentContext::Bolt12Offer + pub fn fields(&$self) -> InvoiceRequestFields { + let InvoiceRequestContents { + payer_signing_pubkey, + inner: InvoiceRequestContentsWithoutPayerSigningPubkey { + quantity, + payer_note, + .. + }, + } = &$inner; + + InvoiceRequestFields { + payer_signing_pubkey: *payer_signing_pubkey, + quantity: *quantity, + payer_note_truncated: payer_note + .clone() + // Truncate the payer note to `PAYER_NOTE_LIMIT` bytes, rounding + // down to the nearest valid UTF-8 code point boundary. + .map(|s| UntrustedString(string_truncate_safe(s, PAYER_NOTE_LIMIT))), + human_readable_name: $self.offer_from_hrn().clone(), + } + } + }; +} + +impl VerifiedInvoiceRequest { offer_accessors!(self, self.inner.contents.inner.offer); invoice_request_accessors!(self, self.inner.contents); + fields_accessor!(self, self.inner.contents); + #[cfg(not(c_bindings))] - invoice_request_respond_with_explicit_signing_pubkey_methods!( + invoice_request_respond_with_derived_signing_pubkey_methods!( self, self.inner, - InvoiceBuilder<'_, ExplicitSigningPubkey> + InvoiceBuilder<'_, DerivedSigningPubkey> ); #[cfg(c_bindings)] - invoice_request_respond_with_explicit_signing_pubkey_methods!( + invoice_request_respond_with_derived_signing_pubkey_methods!( self, self.inner, - InvoiceWithExplicitSigningPubkeyBuilder + InvoiceWithDerivedSigningPubkeyBuilder ); +} + +impl VerifiedInvoiceRequest { + offer_accessors!(self, self.inner.contents.inner.offer); + invoice_request_accessors!(self, self.inner.contents); + fields_accessor!(self, self.inner.contents); + #[cfg(not(c_bindings))] - invoice_request_respond_with_derived_signing_pubkey_methods!( + invoice_request_respond_with_explicit_signing_pubkey_methods!( self, self.inner, - InvoiceBuilder<'_, DerivedSigningPubkey> + InvoiceBuilder<'_, ExplicitSigningPubkey> ); #[cfg(c_bindings)] - invoice_request_respond_with_derived_signing_pubkey_methods!( + invoice_request_respond_with_explicit_signing_pubkey_methods!( self, self.inner, - InvoiceWithDerivedSigningPubkeyBuilder + InvoiceWithExplicitSigningPubkeyBuilder ); +} - /// Fetch the [`InvoiceRequestFields`] for this verified invoice. - /// - /// These are fields which we expect to be useful when receiving a payment for this invoice - /// request, and include the returned [`InvoiceRequestFields`] in the - /// [`PaymentContext::Bolt12Offer`]. - /// - /// [`PaymentContext::Bolt12Offer`]: crate::blinded_path::payment::PaymentContext::Bolt12Offer - pub fn fields(&self) -> InvoiceRequestFields { - let InvoiceRequestContents { - payer_signing_pubkey, - inner: InvoiceRequestContentsWithoutPayerSigningPubkey { quantity, payer_note, .. }, - } = &self.inner.contents; - - InvoiceRequestFields { - payer_signing_pubkey: *payer_signing_pubkey, - quantity: *quantity, - payer_note_truncated: payer_note - .clone() - // Truncate the payer note to `PAYER_NOTE_LIMIT` bytes, rounding - // down to the nearest valid UTF-8 code point boundary. - .map(|s| UntrustedString(string_truncate_safe(s, PAYER_NOTE_LIMIT))), - human_readable_name: self.offer_from_hrn().clone(), - } - } +impl InvoiceRequestVerifiedFromOffer { + offer_accessors!(self, self.inner().contents.inner.offer); + invoice_request_accessors!(self, self.inner().contents); + fields_accessor!(self, self.inner().contents); } /// `String::truncate(new_len)` panics if you split inside a UTF-8 code point, @@ -1631,7 +1772,7 @@ mod tests { .unwrap(); let invoice = invoice_request - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_conversion(payment_paths(), payment_hash(), now()) .unwrap() .experimental_baz(42) .build() @@ -2215,7 +2356,7 @@ mod tests { .features_unchecked(InvoiceRequestFeatures::unknown()) .build_and_sign() .unwrap() - .respond_with_no_std(payment_paths(), payment_hash(), now()) + .respond_with_no_conversion(payment_paths(), payment_hash(), now()) { Ok(_) => panic!("expected error"), Err(e) => assert_eq!(e, Bolt12SemanticError::UnknownRequiredFeatures), @@ -3007,7 +3148,7 @@ mod tests { match invoice_request.verify_using_metadata(&expanded_key, &secp_ctx) { Ok(invoice_request) => { let fields = invoice_request.fields(); - assert_eq!(invoice_request.offer_id, offer.id()); + assert_eq!(invoice_request.offer_id(), offer.id()); assert_eq!( fields, InvoiceRequestFields { diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs index 5b613091c6b..de376b2d2d4 100644 --- a/lightning/src/offers/offer.rs +++ b/lightning/src/offers/offer.rs @@ -1482,7 +1482,7 @@ mod tests { .build_and_sign() .unwrap(); match invoice_request.verify_using_metadata(&expanded_key, &secp_ctx) { - Ok(invoice_request) => assert_eq!(invoice_request.offer_id, offer.id()), + Ok(invoice_request) => assert_eq!(invoice_request.offer_id(), offer.id()), Err(_) => panic!("unexpected error"), } @@ -1563,7 +1563,7 @@ mod tests { .build_and_sign() .unwrap(); match invoice_request.verify_using_recipient_data(nonce, &expanded_key, &secp_ctx) { - Ok(invoice_request) => assert_eq!(invoice_request.offer_id, offer.id()), + Ok(invoice_request) => assert_eq!(invoice_request.offer_id(), offer.id()), Err(_) => panic!("unexpected error"), }