diff --git a/lightning/src/ln/outbound_payment.rs b/lightning/src/ln/outbound_payment.rs index 75fe55bfeac..0592f9b212c 100644 --- a/lightning/src/ln/outbound_payment.rs +++ b/lightning/src/ln/outbound_payment.rs @@ -27,7 +27,7 @@ use crate::offers::invoice_request::InvoiceRequest; use crate::offers::nonce::Nonce; use crate::offers::static_invoice::StaticInvoice; use crate::routing::router::{ - BlindedTail, InFlightHtlcs, Path, PaymentParameters, Route, RouteParameters, + BlindedTail, InFlightHtlcs, Path, Payee, PaymentParameters, Route, RouteParameters, RouteParametersConfig, Router, }; use crate::sign::{EntropySource, NodeSigner, Recipient}; @@ -1413,6 +1413,104 @@ where }) } + // Fallback handler for self-payments when route finding fails + #[rustfmt::skip] + fn handle_self_payment( + &self, payment_id: PaymentId, payment_hash: PaymentHash, recipient_onion: RecipientOnionFields, + keysend_preimage: Option, amount_msat: u64, + pending_events: &Mutex)>>, entropy_source: &ES, + best_block_height: u32, + ) -> Result<(), RetryableSendFailure> + where + ES::Target: EntropySource, + { + // Verify payment secret is provided + if recipient_onion.payment_secret.is_none() { + log_error!(self.logger, "Self-payment requires a payment secret for payment {}. Rejecting.", payment_id); + return Err(RetryableSendFailure::RouteNotFound); + } + + // Use keysend preimage or generate random + // TODO: Integrate inbound_payment::verify for proper validation + let payment_preimage = if let Some(preimage) = keysend_preimage { + preimage + } else { + PaymentPreimage(entropy_source.get_secure_random_bytes()) + }; + + let mut outbounds = self.pending_outbound_payments.lock().unwrap(); + match outbounds.entry(payment_id) { + hash_map::Entry::Occupied(_) => { + log_error!(self.logger, "Payment with id {} is already pending", payment_id); + return Err(RetryableSendFailure::DuplicatePayment); + }, + hash_map::Entry::Vacant(entry) => { + // Track self-payment for history (no HTLCs sent) + entry.insert(PendingOutboundPayment::Retryable { + retry_strategy: None, + attempts: PaymentAttempts::new(), + payment_params: None, + session_privs: new_hash_set(), + payment_hash, + payment_secret: recipient_onion.payment_secret, + payment_metadata: recipient_onion.payment_metadata.clone(), + keysend_preimage: Some(payment_preimage), + invoice_request: None, + bolt12_invoice: None, + custom_tlvs: recipient_onion.custom_tlvs.clone(), + pending_amt_msat: amount_msat, + pending_fee_msat: Some(0), + total_msat: amount_msat, + starting_block_height: best_block_height, + remaining_max_total_routing_fee_msat: None, + }); + } + } + core::mem::drop(outbounds); + + { + let mut outbounds = self.pending_outbound_payments.lock().unwrap(); + if let Some(payment) = outbounds.get_mut(&payment_id) { + payment.mark_fulfilled(); + } + } + + // Generate events for payment sent and claimed + { + let mut pending_events_lock = pending_events.lock().unwrap(); + pending_events_lock.push_back(( + events::Event::PaymentSent { + payment_id: Some(payment_id), + payment_preimage, + payment_hash, + amount_msat: Some(amount_msat), + fee_paid_msat: Some(0), + bolt12_invoice: None, + }, + None, + )); + pending_events_lock.push_back(( + events::Event::PaymentClaimed { + receiver_node_id: None, + payment_hash, + amount_msat, + purpose: events::PaymentPurpose::Bolt11InvoicePayment { + payment_preimage: Some(payment_preimage), + payment_secret: recipient_onion.payment_secret.unwrap(), + }, + htlcs: vec![], + sender_intended_total_msat: Some(amount_msat), + onion_fields: Some(recipient_onion), + payment_id: Some(payment_id), + }, + None, + )); + } + + log_info!(self.logger, "Self-payment with id {} and hash {} succeeded", payment_id, payment_hash); + Ok(()) + } + #[rustfmt::skip] fn find_initial_route( &self, payment_id: PaymentId, payment_hash: PaymentHash, recipient_onion: &RecipientOnionFields, @@ -1484,10 +1582,32 @@ where IH: Fn() -> InFlightHtlcs, SP: Fn(SendAlongPathArgs) -> Result<(), APIError>, { - let route = self.find_initial_route( + let route = match self.find_initial_route( payment_id, payment_hash, &recipient_onion, keysend_preimage, None, &mut route_params, router, &first_hops, &inflight_htlcs, node_signer, best_block_height, - )?; + ) { + Ok(route) => route, + Err(e) => { + // Fallback: check if this is a self-payment + let our_node_id = node_signer.get_node_id(Recipient::Node).unwrap(); + let phantom_node_id = node_signer.get_node_id(Recipient::PhantomNode).ok(); + + let is_self_payment = match &route_params.payment_params.payee { + Payee::Clear { node_id, .. } => { + *node_id == our_node_id || (phantom_node_id == Some(*node_id)) + }, + Payee::Blinded { .. } => false, + }; + + if is_self_payment { + return self.handle_self_payment( + payment_id, payment_hash, recipient_onion, keysend_preimage, route_params.final_value_msat, + pending_events, entropy_source, best_block_height + ); + } + return Err(e); + } + }; let onion_session_privs = self.add_new_pending_payment(payment_hash, recipient_onion.clone(), payment_id, keysend_preimage, &route, Some(retry_strategy), diff --git a/lightning/src/ln/payment_tests.rs b/lightning/src/ln/payment_tests.rs index 8f209c88e25..912e32b0159 100644 --- a/lightning/src/ln/payment_tests.rs +++ b/lightning/src/ln/payment_tests.rs @@ -5407,3 +5407,71 @@ fn max_out_mpp_path() { check_added_monitors(&nodes[0], 2); // one monitor update per MPP part nodes[0].node.get_and_clear_pending_msg_events(); } + +#[test] +fn test_self_payment() { + let chanmon_cfgs = create_chanmon_cfgs(1); + let node_cfgs = create_node_cfgs(1, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(1, &node_cfgs, &[None]); + let nodes = create_network(1, &node_cfgs, &node_chanmgrs); + + let (payment_hash, payment_secret) = + nodes[0].node.create_inbound_payment(Some(100_000), 3600, None).unwrap(); + let amount_msats = 100_000; + let payment_id = PaymentId([1; 32]); + + let route_params = RouteParameters { + payment_params: PaymentParameters::from_node_id(nodes[0].node.get_our_node_id(), 0), + final_value_msat: amount_msats, + max_total_routing_fee_msat: None, + }; + + let recipient_onion_fields = RecipientOnionFields::secret_only(payment_secret); + + // Attempt to send payment to ourselves + nodes[0] + .node + .send_payment( + payment_hash, + recipient_onion_fields, + payment_id, + route_params, + Retry::Attempts(0), + ) + .unwrap(); + + // Process pending events + let events = nodes[0].node.get_and_clear_pending_events(); + + // Should generate both PaymentSent and PaymentClaimed events + let mut payment_sent_found = false; + let mut payment_claimed_found = false; + + for event in events { + match event { + Event::PaymentSent { + payment_id: Some(pid), + payment_hash: ph, + fee_paid_msat: _, + .. + } => { + assert_eq!(pid, payment_id); + assert_eq!(ph, payment_hash); + payment_sent_found = true; + }, + Event::PaymentClaimed { payment_hash: ph, amount_msat: amt, .. } => { + assert_eq!(ph, payment_hash); + assert_eq!(amt, amount_msats); + payment_claimed_found = true; + }, + _ => {}, + } + } + + assert!(payment_sent_found, "PaymentSent event not found"); + assert!(payment_claimed_found, "PaymentClaimed event not found"); + + // Verify payment is now in recent payments + let recent_payments = nodes[0].node.list_recent_payments(); + assert!(!recent_payments.is_empty(), "Self-payment not found in recent payments"); +}