Skip to content

feat: Implement self payment #3934

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 49 additions & 1 deletion lightning/src/ln/channelmanager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4906,6 +4906,7 @@ 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();
Expand All @@ -4920,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));
}
Comment on lines +4957 to +4966
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The implementation currently generates PaymentSent events only for spontaneous payments (with keysend_preimage), but not for Bolt11 invoice payments. This creates inconsistent behavior where Bolt11 self-payments will appear as claimable but never as sent.

For consistency in the payment lifecycle, both types of self-payments should generate both events. Consider modifying the code to handle Bolt11 self-payments similarly to spontaneous payments by generating the appropriate PaymentSent event when the payment is claimed.

Suggested change
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));
}
// Generate PaymentSent event for both keysend and Bolt11 self-payments
pending_events.push_back((events::Event::PaymentSent {
payment_id: Some(payment_id),
payment_preimage: keysend_preimage.unwrap_or_else(|| payment_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));

Spotted by Diamond

Is this helpful? React 👍 or 👎 to let us know.

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 => {
Expand Down Expand Up @@ -7704,7 +7751,8 @@ where
ComplFunc: FnOnce(
Option<u64>,
bool,
) -> (Option<MonitorUpdateCompletionAction>, Option<RAAMonitorUpdateBlockingAction>),
)
-> (Option<MonitorUpdateCompletionAction>, Option<RAAMonitorUpdateBlockingAction>),
>(
&self, prev_hop: HTLCPreviousHopData, payment_preimage: PaymentPreimage,
payment_info: Option<PaymentClaimDetails>, completion_action: ComplFunc,
Expand Down
52 changes: 52 additions & 0 deletions lightning/src/ln/payment_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
25 changes: 21 additions & 4 deletions lightning/src/routing/router.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@ 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};
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,
};
Expand All @@ -37,9 +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};
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -3727,6 +3726,24 @@ where L::Target: Logger {
Ok(route)
}

fn create_self_payment_route(
our_node_pubkey: &PublicKey, route_params: &RouteParameters,
) -> Result<Route, &'static str> {
let path = Path {
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
// 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
Expand Down
Loading