From 7d78e4e81227c5375d6ec672d48a43b2fde92ad4 Mon Sep 17 00:00:00 2001 From: Prabhat Verma Date: Wed, 16 Jul 2025 02:18:35 +0530 Subject: [PATCH 1/3] add rough logic for self payment --- lightning/src/ln/channelmanager.rs | 3 +++ lightning/src/routing/router.rs | 22 ++++++++++++++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 8c034b23343..888a42ab4d9 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -4906,6 +4906,9 @@ where path, payment_hash, recipient_onion, total_value, cur_height, payment_id, keysend_preimage, invoice_request, bolt12_invoice, session_priv_bytes } = args; + + + // The top-level caller should hold the total_consistency_lock read lock. debug_assert!(self.total_consistency_lock.try_write().is_err()); let prng_seed = self.entropy_source.get_secure_random_bytes(); diff --git a/lightning/src/routing/router.rs b/lightning/src/routing/router.rs index 5ad5c3e7786..952484a3685 100644 --- a/lightning/src/routing/router.rs +++ b/lightning/src/routing/router.rs @@ -37,7 +37,6 @@ use crate::types::features::{ use crate::types::payment::{PaymentHash, PaymentPreimage}; use crate::util::logger::Logger; use crate::util::ser::{Readable, ReadableArgs, Writeable, Writer}; - use crate::io; use crate::prelude::*; use alloc::collections::BinaryHeap; @@ -2453,7 +2452,7 @@ where L::Target: Logger { let our_node_id = NodeId::from_pubkey(&our_node_pubkey); if payee_node_id_opt.map_or(false, |payee| payee == our_node_id) { - return Err("Cannot generate a route to ourselves"); + return create_self_payment_route(our_node_pubkey, route_params); } if our_node_id == maybe_dummy_payee_node_id { return Err("Invalid origin node id provided, use a different one"); @@ -3727,6 +3726,25 @@ where L::Target: Logger { Ok(route) } +fn create_self_payment_route(our_node_pubkey: &PublicKey, route_params: &RouteParameters) -> Result { + let path = Path { + hops: vec![RouteHop { + pubkey: our_node_pubkey.clone(), + short_channel_id: 0 , // Dummy short_channel_id specifying self payment + fee_msat: 0, // Zero fees + cltv_expiry_delta: MIN_FINAL_CLTV_EXPIRY_DELTA.into(), + node_features: NodeFeatures::empty(), + channel_features: ChannelFeatures::empty(), + maybe_announced_channel: false, + }], + blinded_tail: None, + }; + Ok(Route { + paths: vec![path], + route_params: Some(route_params.clone()), + }) +} + // When an adversarial intermediary node observes a payment, it may be able to infer its // destination, if the remaining CLTV expiry delta exactly matches a feasible path in the network // graph. In order to improve privacy, this method obfuscates the CLTV expiry deltas along the From d842a753480c3a6da7e664918acad16842318744 Mon Sep 17 00:00:00 2001 From: Prabhat Verma Date: Wed, 23 Jul 2025 19:09:19 +0530 Subject: [PATCH 2/3] handle scid_to_chan lookup error Normal lightning workflow does not know how to handle self payment as the route created is to self . This will not be found in short_to_chan_info as there cant be channel created where both the nodes are the same. This handles that scenario by handling the payment before the lookup is hit --- lightning/src/ln/channelmanager.rs | 51 ++++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 888a42ab4d9..d402211830d 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -4907,8 +4907,6 @@ where invoice_request, bolt12_invoice, session_priv_bytes } = args; - - // The top-level caller should hold the total_consistency_lock read lock. debug_assert!(self.total_consistency_lock.try_write().is_err()); let prng_seed = self.entropy_source.get_secure_random_bytes(); @@ -4923,6 +4921,52 @@ where e })?; + // Check if this is a self-payment (indicated by short_channel_id == 0) + if path.hops.len() == 1 && path.hops.first().unwrap().short_channel_id == 0 { + // This is a self-payment, handle it directly + let logger = WithContext::from(&self.logger, Some(self.get_our_node_id()), None, Some(*payment_hash)); + log_trace!(logger, "Processing self-payment with payment hash {}", payment_hash); + // For self-payments, we immediately generate the PaymentClaimable event + // since we are both the sender and receiver + let mut pending_events = self.pending_events.lock().unwrap(); + // Generate PaymentClaimable event + let purpose = if let Some(preimage) = keysend_preimage { + events::PaymentPurpose::SpontaneousPayment(*preimage) + } else if let Some(payment_secret) = recipient_onion.payment_secret { + events::PaymentPurpose::Bolt11InvoicePayment { + payment_preimage: None, + payment_secret, + } + } else { + return Err(APIError::APIMisuseError{ + err: "Self-payment requires either keysend preimage or payment secret".to_owned() + }); + }; + pending_events.push_back((events::Event::PaymentClaimable { + receiver_node_id: Some(self.get_our_node_id()), + payment_hash: *payment_hash, + onion_fields: Some(recipient_onion.clone()), + amount_msat: htlc_msat, + counterparty_skimmed_fee_msat: 0, + purpose, + via_channel_ids: Vec::new(), + claim_deadline: None, + payment_id: Some(payment_id), + }, None)); + // For spontaneous payments, also generate PaymentSent event immediately + if keysend_preimage.is_some() { + pending_events.push_back((events::Event::PaymentSent { + payment_id: Some(payment_id), + payment_preimage: keysend_preimage.unwrap(), + payment_hash: *payment_hash, + amount_msat: Some(htlc_msat), + fee_paid_msat: Some(0), // No fees for self-payments + bolt12_invoice: None, + }, None)); + } + return Ok(()); + } + let err: Result<(), _> = loop { let (counterparty_node_id, id) = match self.short_to_chan_info.read().unwrap().get(&path.hops.first().unwrap().short_channel_id) { None => { @@ -7707,7 +7751,8 @@ where ComplFunc: FnOnce( Option, bool, - ) -> (Option, Option), + ) + -> (Option, Option), >( &self, prev_hop: HTLCPreviousHopData, payment_preimage: PaymentPreimage, payment_info: Option, completion_action: ComplFunc, From 39643a57d78f3f2b5cf9d10285dd2be2d30ce6bb Mon Sep 17 00:00:00 2001 From: Prabhat Verma Date: Wed, 23 Jul 2025 19:09:37 +0530 Subject: [PATCH 3/3] add test for self-payment --- lightning/src/ln/payment_tests.rs | 52 +++++++++++++++++++++++++++++++ lightning/src/routing/router.rs | 35 ++++++++++----------- 2 files changed, 69 insertions(+), 18 deletions(-) diff --git a/lightning/src/ln/payment_tests.rs b/lightning/src/ln/payment_tests.rs index 503b040de41..8cbbe3970a5 100644 --- a/lightning/src/ln/payment_tests.rs +++ b/lightning/src/ln/payment_tests.rs @@ -5259,3 +5259,55 @@ 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(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let self_node = nodes[0].node.get_our_node_id(); + let _other_node = nodes[1].node.get_our_node_id(); + + create_announced_chan_between_nodes(&nodes, 0, 1); + + let amt_msat = 10_000; + let payment_params = PaymentParameters::from_node_id(self_node, TEST_FINAL_CLTV) + .with_bolt11_features(nodes[0].node.bolt11_invoice_features()) + .unwrap(); + let route_params = RouteParameters::from_payment_params_and_value(payment_params, amt_msat); + + let preimage = Some(PaymentPreimage([42; 32])); + let onion = RecipientOnionFields::spontaneous_empty(); + let retry = Retry::Attempts(0); // payment to self should not be retried , it should succeed in one go ideally + let id = PaymentId([42; 32]); + nodes[0].node.send_spontaneous_payment(preimage, onion, id, route_params, retry).unwrap(); + + check_added_monitors!(nodes[0], 0); // Self-payments don't add monitors since no actual channel update occurs + + let events = nodes[0].node.get_and_clear_pending_events(); + assert_eq!(events.len(), 2); + + let mut claimable_found = false; + let mut sent_found = false; + + for event in &events { + match event { + Event::PaymentClaimable { purpose, .. } => { + if let PaymentPurpose::SpontaneousPayment(_preimage) = purpose { + // For self-payments, we don't need to manually claim since the payment + // is automatically processed. The PaymentSent event is already generated. + claimable_found = true; + } + }, + Event::PaymentSent { .. } => { + sent_found = true; + }, + _ => panic!("Unexpected event: {:?}", event), + } + } + + assert!(claimable_found, "Expected PaymentClaimable event"); + assert!(sent_found, "Expected PaymentSent event"); +} diff --git a/lightning/src/routing/router.rs b/lightning/src/routing/router.rs index 952484a3685..9bf91cfec28 100644 --- a/lightning/src/routing/router.rs +++ b/lightning/src/routing/router.rs @@ -18,6 +18,7 @@ use crate::blinded_path::payment::{ }; use crate::blinded_path::{BlindedHop, Direction, IntroductionNode}; use crate::crypto::chacha20::ChaCha20; +use crate::io; use crate::ln::channel_state::ChannelDetails; use crate::ln::channelmanager::{PaymentId, RecipientOnionFields, MIN_FINAL_CLTV_EXPIRY_DELTA}; use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT}; @@ -25,6 +26,7 @@ use crate::ln::onion_utils; use crate::offers::invoice::Bolt12Invoice; #[cfg(async_payments)] use crate::offers::static_invoice::StaticInvoice; +use crate::prelude::*; use crate::routing::gossip::{ DirectedChannelInfo, EffectiveCapacity, NetworkGraph, NodeId, ReadOnlyNetworkGraph, }; @@ -37,8 +39,6 @@ use crate::types::features::{ use crate::types::payment::{PaymentHash, PaymentPreimage}; use crate::util::logger::Logger; use crate::util::ser::{Readable, ReadableArgs, Writeable, Writer}; -use crate::io; -use crate::prelude::*; use alloc::collections::BinaryHeap; use core::ops::Deref; use core::{cmp, fmt}; @@ -3726,23 +3726,22 @@ where L::Target: Logger { Ok(route) } -fn create_self_payment_route(our_node_pubkey: &PublicKey, route_params: &RouteParameters) -> Result { +fn create_self_payment_route( + our_node_pubkey: &PublicKey, route_params: &RouteParameters, +) -> Result { let path = Path { - hops: vec![RouteHop { - pubkey: our_node_pubkey.clone(), - short_channel_id: 0 , // Dummy short_channel_id specifying self payment - fee_msat: 0, // Zero fees - cltv_expiry_delta: MIN_FINAL_CLTV_EXPIRY_DELTA.into(), - node_features: NodeFeatures::empty(), - channel_features: ChannelFeatures::empty(), - maybe_announced_channel: false, - }], - blinded_tail: None, - }; - Ok(Route { - paths: vec![path], - route_params: Some(route_params.clone()), - }) + hops: vec![RouteHop { + pubkey: our_node_pubkey.clone(), + short_channel_id: 0, // Dummy short_channel_id specifying self payment + fee_msat: route_params.final_value_msat, // last hop send the entire amount + cltv_expiry_delta: MIN_FINAL_CLTV_EXPIRY_DELTA.into(), + node_features: NodeFeatures::empty(), + channel_features: ChannelFeatures::empty(), + maybe_announced_channel: false, + }], + blinded_tail: None, + }; + Ok(Route { paths: vec![path], route_params: Some(route_params.clone()) }) } // When an adversarial intermediary node observes a payment, it may be able to infer its