Skip to content

Commit c2ee56f

Browse files
Support initiating an async payment to a static invoice.
Supported when the sender is an always-online node. Often-offline senders will need to lock in their HTLC(s) with their always-online channel counterparty and modify the held_htlc_available reply path to terminate at said counterparty.
1 parent 1a9211d commit c2ee56f

File tree

2 files changed

+80
-8
lines changed

2 files changed

+80
-8
lines changed

lightning/src/ln/channelmanager.rs

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ use crate::offers::offer::{Offer, OfferBuilder};
6767
use crate::offers::parse::Bolt12SemanticError;
6868
use crate::offers::refund::{Refund, RefundBuilder};
6969
use crate::onion_message::async_payments::{AsyncPaymentsMessage, HeldHtlcAvailable, ReleaseHeldHtlc, AsyncPaymentsMessageHandler};
70+
#[cfg(async_payments)]
71+
use crate::offers::static_invoice::StaticInvoice;
7072
use crate::onion_message::messenger::{new_pending_onion_message, Destination, MessageRouter, PendingOnionMessage, Responder, ResponseInstruction};
7173
use crate::onion_message::offers::{OffersMessage, OffersMessageHandler};
7274
use crate::sign::{EntropySource, NodeSigner, Recipient, SignerProvider};
@@ -4046,6 +4048,34 @@ where
40464048
)
40474049
}
40484050

4051+
#[cfg(async_payments)]
4052+
fn initiate_async_payment(
4053+
&self, invoice: &StaticInvoice, payment_id: PaymentId
4054+
) -> Result<(), InvoiceError> {
4055+
if invoice.message_paths().is_empty() { return Err(Bolt12SemanticError::MissingPaths.into()) }
4056+
4057+
let reply_path = self.create_blinded_path(Some(payment_id))
4058+
.map_err(|_| Bolt12SemanticError::MissingPaths)?;
4059+
4060+
let _persistence_guard = PersistenceNotifierGuard::notify_on_drop(self);
4061+
let payment_release_secret = self.pending_outbound_payments.static_invoice_received(
4062+
invoice, payment_id, &*self.entropy_source
4063+
).map_err(|e| InvoiceError::from_string(format!("{:?}", e)))?;
4064+
4065+
let mut pending_async_payments_messages = self.pending_async_payments_messages.lock().unwrap();
4066+
const HTLC_AVAILABLE_LIMIT: usize = 10;
4067+
for path in invoice.message_paths().into_iter().take(HTLC_AVAILABLE_LIMIT) {
4068+
let message = new_pending_onion_message(
4069+
AsyncPaymentsMessage::HeldHtlcAvailable(HeldHtlcAvailable { payment_release_secret }),
4070+
Destination::BlindedPath(path.clone()),
4071+
Some(reply_path.clone()),
4072+
);
4073+
pending_async_payments_messages.push(message);
4074+
}
4075+
4076+
Ok(())
4077+
}
4078+
40494079
/// Signals that no further attempts for the given payment should occur. Useful if you have a
40504080
/// pending outbound payment with retries remaining, but wish to stop retrying the payment before
40514081
/// retries are exhausted.
@@ -10389,14 +10419,22 @@ where
1038910419
}
1039010420
},
1039110421
#[cfg(async_payments)]
10392-
OffersMessage::StaticInvoice(_invoice) => {
10393-
match responder {
10394-
Some(responder) => {
10395-
responder.respond(OffersMessage::InvoiceError(
10396-
InvoiceError::from_string("Static invoices not yet supported".to_string())
10397-
))
10398-
},
10422+
OffersMessage::StaticInvoice(invoice) => {
10423+
let responder = match responder {
10424+
Some(responder) => responder,
1039910425
None => return ResponseInstruction::NoResponse,
10426+
};
10427+
let payment_id = match _payment_id {
10428+
Some(id) => id,
10429+
None => {
10430+
return responder.respond(OffersMessage::InvoiceError(
10431+
InvoiceError::from_string("Unrecognized invoice".to_string())
10432+
))
10433+
}
10434+
};
10435+
match self.initiate_async_payment(&invoice, payment_id) {
10436+
Ok(()) => return ResponseInstruction::NoResponse,
10437+
Err(e) => responder.respond(OffersMessage::InvoiceError(e)),
1040010438
}
1040110439
},
1040210440
OffersMessage::InvoiceError(invoice_error) => {
@@ -10432,7 +10470,7 @@ where
1043210470
fn release_held_htlc(&self, _message: ReleaseHeldHtlc, _payment_id: Option<PaymentId>) {}
1043310471

1043410472
fn release_pending_messages(&self) -> Vec<PendingOnionMessage<AsyncPaymentsMessage>> {
10435-
Vec::new()
10473+
core::mem::take(&mut self.pending_async_payments_messages.lock().unwrap())
1043610474
}
1043710475
}
1043810476

lightning/src/ln/outbound_payment.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ use crate::ln::onion_utils;
2323
use crate::ln::onion_utils::{DecodedOnionFailure, HTLCFailReason};
2424
use crate::offers::invoice::Bolt12Invoice;
2525
use crate::offers::invoice_request::InvoiceRequest;
26+
#[cfg(async_payments)]
27+
use crate::offers::static_invoice::StaticInvoice;
2628
use crate::routing::router::{BlindedTail, InFlightHtlcs, Path, PaymentParameters, Route, RouteParameters, Router};
2729
use crate::sign::{EntropySource, NodeSigner, Recipient};
2830
use crate::util::errors::APIError;
@@ -878,6 +880,38 @@ impl OutboundPayments {
878880
Ok(())
879881
}
880882

883+
#[cfg(async_payments)]
884+
pub(super) fn static_invoice_received<ES: Deref>(
885+
&self, invoice: &StaticInvoice, payment_id: PaymentId, entropy_source: ES
886+
) -> Result<[u8; 32], Bolt12PaymentError> where ES::Target: EntropySource {
887+
match self.pending_outbound_payments.lock().unwrap().entry(payment_id) {
888+
hash_map::Entry::Occupied(entry) => match entry.get() {
889+
PendingOutboundPayment::AwaitingInvoice { retry_strategy, invoice_request, .. } => {
890+
let invreq = invoice_request.as_ref().ok_or(Bolt12PaymentError::UnexpectedInvoice)?;
891+
if !invoice.matches_invreq(invreq) {
892+
return Err(Bolt12PaymentError::UnexpectedInvoice)
893+
}
894+
let amount_msat = invreq.amount_msats().ok_or(Bolt12PaymentError::UnexpectedInvoice)?;
895+
let keysend_preimage = PaymentPreimage(entropy_source.get_secure_random_bytes());
896+
let payment_hash = PaymentHash(Sha256::hash(&keysend_preimage.0).to_byte_array());
897+
let payment_release_secret = entropy_source.get_secure_random_bytes();
898+
let pay_params = PaymentParameters::from_static_invoice(invoice);
899+
let route_params = RouteParameters::from_payment_params_and_value(pay_params, amount_msat);
900+
*entry.into_mut() = PendingOutboundPayment::StaticInvoiceReceived {
901+
payment_hash,
902+
keysend_preimage,
903+
retry_strategy: *retry_strategy,
904+
payment_release_secret,
905+
route_params,
906+
};
907+
return Ok(payment_release_secret)
908+
},
909+
_ => return Err(Bolt12PaymentError::DuplicateInvoice),
910+
},
911+
hash_map::Entry::Vacant(_) => return Err(Bolt12PaymentError::UnexpectedInvoice),
912+
};
913+
}
914+
881915
pub(super) fn check_retry_payments<R: Deref, ES: Deref, NS: Deref, SP, IH, FH, L: Deref>(
882916
&self, router: &R, first_hops: FH, inflight_htlcs: IH, entropy_source: &ES, node_signer: &NS,
883917
best_block_height: u32,

0 commit comments

Comments
 (0)