Skip to content

Commit 9f8f138

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 392815e commit 9f8f138

File tree

2 files changed

+105
-8
lines changed

2 files changed

+105
-8
lines changed

lightning/src/ln/channelmanager.rs

Lines changed: 71 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,9 @@ use crate::offers::invoice_request::{DerivedPayerId, InvoiceRequestBuilder};
6666
use crate::offers::offer::{Offer, OfferBuilder};
6767
use crate::offers::parse::Bolt12SemanticError;
6868
use crate::offers::refund::{Refund, RefundBuilder};
69-
use crate::onion_message::async_payments::AsyncPaymentsMessage;
69+
#[cfg(async_payments)]
70+
use crate::offers::static_invoice::StaticInvoice;
71+
use crate::onion_message::async_payments::{AsyncPaymentsMessage, AsyncPaymentsMessageHandler, HeldHtlcAvailable, ReleaseHeldHtlc};
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};
@@ -4045,6 +4047,34 @@ where
40454047
)
40464048
}
40474049

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

10451+
impl<M: Deref, T: Deref, ES: Deref, NS: Deref, SP: Deref, F: Deref, R: Deref, L: Deref>
10452+
AsyncPaymentsMessageHandler for ChannelManager<M, T, ES, NS, SP, F, R, L>
10453+
where
10454+
M::Target: chain::Watch<<SP::Target as SignerProvider>::EcdsaSigner>,
10455+
T::Target: BroadcasterInterface,
10456+
ES::Target: EntropySource,
10457+
NS::Target: NodeSigner,
10458+
SP::Target: SignerProvider,
10459+
F::Target: FeeEstimator,
10460+
R::Target: Router,
10461+
L::Target: Logger,
10462+
{
10463+
fn held_htlc_available(
10464+
&self, _message: HeldHtlcAvailable, _responder: Option<Responder>
10465+
) -> ResponseInstruction<ReleaseHeldHtlc> {
10466+
ResponseInstruction::NoResponse
10467+
}
10468+
10469+
fn release_held_htlc(&self, _message: ReleaseHeldHtlc, _payment_id: Option<PaymentId>) {}
10470+
10471+
fn release_pending_messages(&self) -> Vec<PendingOnionMessage<AsyncPaymentsMessage>> {
10472+
core::mem::take(&mut self.pending_async_payments_messages.lock().unwrap())
10473+
}
10474+
}
10475+
1041310476
impl<M: Deref, T: Deref, ES: Deref, NS: Deref, SP: Deref, F: Deref, R: Deref, L: Deref>
1041410477
NodeIdLookUp for ChannelManager<M, T, ES, NS, SP, F, R, L>
1041510478
where

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;
@@ -864,6 +866,38 @@ impl OutboundPayments {
864866
Ok(())
865867
}
866868

869+
#[cfg(async_payments)]
870+
pub(super) fn static_invoice_received<ES: Deref>(
871+
&self, invoice: &StaticInvoice, payment_id: PaymentId, entropy_source: ES
872+
) -> Result<[u8; 32], Bolt12PaymentError> where ES::Target: EntropySource {
873+
match self.pending_outbound_payments.lock().unwrap().entry(payment_id) {
874+
hash_map::Entry::Occupied(entry) => match entry.get() {
875+
PendingOutboundPayment::AwaitingInvoice { retry_strategy, invoice_request, .. } => {
876+
let invreq = invoice_request.as_ref().ok_or(Bolt12PaymentError::UnexpectedInvoice)?;
877+
if !invoice.matches_invreq(invreq) {
878+
return Err(Bolt12PaymentError::UnexpectedInvoice)
879+
}
880+
let amount_msat = invreq.amount_msats().ok_or(Bolt12PaymentError::UnexpectedInvoice)?;
881+
let keysend_preimage = PaymentPreimage(entropy_source.get_secure_random_bytes());
882+
let payment_hash = PaymentHash(Sha256::hash(&keysend_preimage.0).to_byte_array());
883+
let payment_release_secret = entropy_source.get_secure_random_bytes();
884+
let pay_params = PaymentParameters::from_static_invoice(invoice);
885+
let route_params = RouteParameters::from_payment_params_and_value(pay_params, amount_msat);
886+
*entry.into_mut() = PendingOutboundPayment::StaticInvoiceReceived {
887+
payment_hash,
888+
keysend_preimage,
889+
retry_strategy: *retry_strategy,
890+
payment_release_secret,
891+
route_params,
892+
};
893+
return Ok(payment_release_secret)
894+
},
895+
_ => return Err(Bolt12PaymentError::DuplicateInvoice),
896+
},
897+
hash_map::Entry::Vacant(_) => return Err(Bolt12PaymentError::UnexpectedInvoice),
898+
};
899+
}
900+
867901
pub(super) fn check_retry_payments<R: Deref, ES: Deref, NS: Deref, SP, IH, FH, L: Deref>(
868902
&self, router: &R, first_hops: FH, inflight_htlcs: IH, entropy_source: &ES, node_signer: &NS,
869903
best_block_height: u32,

0 commit comments

Comments
 (0)