Skip to content

Commit 1309a30

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 5919a57 commit 1309a30

File tree

2 files changed

+81
-8
lines changed

2 files changed

+81
-8
lines changed

lightning/src/ln/channelmanager.rs

+47-8
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ use crate::offers::offer::{Offer, OfferBuilder};
6868
use crate::offers::parse::Bolt12SemanticError;
6969
use crate::offers::refund::{Refund, RefundBuilder};
7070
use crate::onion_message::async_payments::{AsyncPaymentsMessage, HeldHtlcAvailable, ReleaseHeldHtlc, AsyncPaymentsMessageHandler};
71+
#[cfg(async_payments)]
72+
use crate::offers::static_invoice::StaticInvoice;
7173
use crate::onion_message::messenger::{new_pending_onion_message, Destination, MessageRouter, PendingOnionMessage, Responder, ResponseInstruction};
7274
use crate::onion_message::offers::{OffersMessage, OffersMessageHandler};
7375
use crate::sign::{EntropySource, NodeSigner, Recipient, SignerProvider};
@@ -4130,6 +4132,35 @@ where
41304132
)
41314133
}
41324134

4135+
#[cfg(async_payments)]
4136+
fn initiate_async_payment(
4137+
&self, invoice: &StaticInvoice, payment_id: PaymentId
4138+
) -> Result<(), InvoiceError> {
4139+
if invoice.message_paths().is_empty() { return Err(Bolt12SemanticError::MissingPaths.into()) }
4140+
4141+
let reply_path = self.create_blinded_path(
4142+
MessageContext::AsyncPayments(AsyncPaymentsContext::OutboundPayment { payment_id })
4143+
).map_err(|_| Bolt12SemanticError::MissingPaths)?;
4144+
4145+
let _persistence_guard = PersistenceNotifierGuard::notify_on_drop(self);
4146+
let payment_release_secret = self.pending_outbound_payments.static_invoice_received(
4147+
invoice, payment_id, &*self.entropy_source
4148+
).map_err(|e| InvoiceError::from_string(format!("{:?}", e)))?;
4149+
4150+
let mut pending_async_payments_messages = self.pending_async_payments_messages.lock().unwrap();
4151+
const HTLC_AVAILABLE_LIMIT: usize = 10;
4152+
for path in invoice.message_paths().into_iter().take(HTLC_AVAILABLE_LIMIT) {
4153+
let message = new_pending_onion_message(
4154+
AsyncPaymentsMessage::HeldHtlcAvailable(HeldHtlcAvailable { payment_release_secret }),
4155+
Destination::BlindedPath(path.clone()),
4156+
Some(reply_path.clone()),
4157+
);
4158+
pending_async_payments_messages.push(message);
4159+
}
4160+
4161+
Ok(())
4162+
}
4163+
41334164
/// Signals that no further attempts for the given payment should occur. Useful if you have a
41344165
/// pending outbound payment with retries remaining, but wish to stop retrying the payment before
41354166
/// retries are exhausted.
@@ -10652,14 +10683,22 @@ where
1065210683
}
1065310684
},
1065410685
#[cfg(async_payments)]
10655-
OffersMessage::StaticInvoice(_invoice) => {
10656-
match responder {
10657-
Some(responder) => {
10658-
responder.respond(OffersMessage::InvoiceError(
10659-
InvoiceError::from_string("Static invoices not yet supported".to_string())
10660-
))
10661-
},
10686+
OffersMessage::StaticInvoice(invoice) => {
10687+
let responder = match responder {
10688+
Some(responder) => responder,
1066210689
None => return ResponseInstruction::NoResponse,
10690+
};
10691+
let payment_id = match context {
10692+
OffersContext::OutboundPayment { payment_id } => payment_id,
10693+
_ => {
10694+
return responder.respond(OffersMessage::InvoiceError(
10695+
InvoiceError::from_string("Unrecognized invoice".to_string())
10696+
))
10697+
}
10698+
};
10699+
match self.initiate_async_payment(&invoice, payment_id) {
10700+
Ok(()) => return ResponseInstruction::NoResponse,
10701+
Err(e) => responder.respond(OffersMessage::InvoiceError(e)),
1066310702
}
1066410703
},
1066510704
OffersMessage::InvoiceError(invoice_error) => {
@@ -10696,7 +10735,7 @@ where
1069610735
fn release_held_htlc(&self, _message: ReleaseHeldHtlc, _context: AsyncPaymentsContext) {}
1069710736

1069810737
fn release_pending_messages(&self) -> Vec<PendingOnionMessage<AsyncPaymentsMessage>> {
10699-
Vec::new()
10738+
core::mem::take(&mut self.pending_async_payments_messages.lock().unwrap())
1070010739
}
1070110740
}
1070210741

lightning/src/ln/outbound_payment.rs

+34
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)