Skip to content

Commit b6f4479

Browse files
Support initiating an async payment to a static invoice.
Supported when the sender is an always-online node. Here we send the initial held_htlc_available onion message upon receipt of a static invoice, next we'll need to actually send HTLCs upon getting a response to said OM.
1 parent c976e4c commit b6f4479

File tree

2 files changed

+158
-6
lines changed

2 files changed

+158
-6
lines changed

lightning/src/ln/channelmanager.rs

Lines changed: 88 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ use crate::offers::offer::{Offer, OfferBuilder};
7171
use crate::offers::parse::Bolt12SemanticError;
7272
use crate::offers::refund::{Refund, RefundBuilder};
7373
use crate::offers::signer;
74+
#[cfg(async_payments)]
75+
use crate::offers::static_invoice::StaticInvoice;
7476
use crate::onion_message::async_payments::{AsyncPaymentsMessage, HeldHtlcAvailable, ReleaseHeldHtlc, AsyncPaymentsMessageHandler};
7577
use crate::onion_message::messenger::{Destination, MessageRouter, Responder, ResponseInstruction, MessageSendInstructions};
7678
use crate::onion_message::offers::{OffersMessage, OffersMessageHandler};
@@ -4318,6 +4320,61 @@ where
43184320
)
43194321
}
43204322

4323+
#[cfg(async_payments)]
4324+
fn initiate_async_payment(
4325+
&self, invoice: &StaticInvoice, payment_id: PaymentId
4326+
) -> Result<(), Bolt12PaymentError> {
4327+
let mut res = Ok(());
4328+
PersistenceNotifierGuard::optionally_notify(self, || {
4329+
let outbound_pmts_res = self.pending_outbound_payments.static_invoice_received(
4330+
invoice, payment_id, &*self.entropy_source, &self.pending_events
4331+
);
4332+
let payment_release_secret = match outbound_pmts_res {
4333+
Ok(secret) => secret,
4334+
Err(Bolt12PaymentError::UnexpectedInvoice) | Err(Bolt12PaymentError::DuplicateInvoice) => {
4335+
res = outbound_pmts_res.map(|_| ());
4336+
return NotifyOption::SkipPersistNoEvents
4337+
},
4338+
Err(e) => {
4339+
res = Err(e);
4340+
return NotifyOption::DoPersist
4341+
}
4342+
};
4343+
4344+
let reply_paths = match self.create_blinded_paths(
4345+
MessageContext::AsyncPayments(AsyncPaymentsContext::OutboundPayment { payment_id })
4346+
) {
4347+
Ok(paths) => paths,
4348+
Err(()) => {
4349+
self.abandon_payment_with_reason(payment_id, PaymentFailureReason::RouteNotFound);
4350+
res = Err(Bolt12PaymentError::SendingFailed(RetryableSendFailure::RouteNotFound));
4351+
return NotifyOption::DoPersist
4352+
}
4353+
};
4354+
4355+
let mut pending_async_payments_messages = self.pending_async_payments_messages.lock().unwrap();
4356+
const HTLC_AVAILABLE_LIMIT: usize = 10;
4357+
reply_paths
4358+
.iter()
4359+
.flat_map(|reply_path| invoice.message_paths().iter().map(move |invoice_path| (invoice_path, reply_path)))
4360+
.take(HTLC_AVAILABLE_LIMIT)
4361+
.for_each(|(invoice_path, reply_path)| {
4362+
let instructions = MessageSendInstructions::WithSpecifiedReplyPath {
4363+
destination: Destination::BlindedPath(invoice_path.clone()),
4364+
reply_path: reply_path.clone(),
4365+
};
4366+
let message = AsyncPaymentsMessage::HeldHtlcAvailable(
4367+
HeldHtlcAvailable { payment_release_secret }
4368+
);
4369+
pending_async_payments_messages.push((message, instructions));
4370+
});
4371+
4372+
NotifyOption::DoPersist
4373+
});
4374+
4375+
res
4376+
}
4377+
43214378
/// Signals that no further attempts for the given payment should occur. Useful if you have a
43224379
/// pending outbound payment with retries remaining, but wish to stop retrying the payment before
43234380
/// retries are exhausted.
@@ -11040,14 +11097,39 @@ where
1104011097
}
1104111098
},
1104211099
#[cfg(async_payments)]
11043-
OffersMessage::StaticInvoice(_invoice) => {
11100+
OffersMessage::StaticInvoice(invoice) => {
11101+
let payment_id = match context {
11102+
Some(OffersContext::OutboundPayment { payment_id, nonce, hmac: Some(hmac) }) => {
11103+
if payment_id.verify(hmac, nonce, expanded_key).is_err() {
11104+
return None
11105+
}
11106+
payment_id
11107+
},
11108+
_ => return None
11109+
};
11110+
// TODO: DRY this with the above regular invoice error handling
11111+
let error = match self.initiate_async_payment(&invoice, payment_id) {
11112+
Err(Bolt12PaymentError::UnknownRequiredFeatures) => {
11113+
log_trace!(
11114+
self.logger, "Invoice requires unknown features: {:?}",
11115+
invoice.invoice_features()
11116+
);
11117+
InvoiceError::from(Bolt12SemanticError::UnknownRequiredFeatures)
11118+
},
11119+
Err(Bolt12PaymentError::SendingFailed(e)) => {
11120+
log_trace!(self.logger, "Failed paying invoice: {:?}", e);
11121+
InvoiceError::from_string(format!("{:?}", e))
11122+
},
11123+
Err(Bolt12PaymentError::UnexpectedInvoice)
11124+
| Err(Bolt12PaymentError::DuplicateInvoice)
11125+
| Ok(()) => return None,
11126+
};
1104411127
match responder {
11045-
Some(responder) => {
11046-
return Some((OffersMessage::InvoiceError(
11047-
InvoiceError::from_string("Static invoices not yet supported".to_string())
11048-
), responder.respond()));
11128+
Some(responder) => Some((OffersMessage::InvoiceError(error), responder.respond())),
11129+
None => {
11130+
log_trace!(self.logger, "No reply path to send error: {:?}", error);
11131+
None
1104911132
},
11050-
None => return None,
1105111133
}
1105211134
},
1105311135
OffersMessage::InvoiceError(invoice_error) => {

lightning/src/ln/outbound_payment.rs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@ use crate::util::logger::Logger;
3232
use crate::util::time::Instant;
3333
use crate::util::ser::ReadableArgs;
3434

35+
#[cfg(async_payments)]
36+
use {
37+
crate::offers::invoice::{DerivedSigningPubkey, InvoiceBuilder},
38+
crate::offers::static_invoice::StaticInvoice,
39+
};
40+
3541
use core::fmt::{self, Display, Formatter};
3642
use core::ops::Deref;
3743
use core::sync::atomic::{AtomicBool, Ordering};
@@ -928,6 +934,70 @@ impl OutboundPayments {
928934
Ok(())
929935
}
930936

937+
#[cfg(async_payments)]
938+
pub(super) fn static_invoice_received<ES: Deref>(
939+
&self, invoice: &StaticInvoice, payment_id: PaymentId, entropy_source: ES,
940+
pending_events: &Mutex<VecDeque<(events::Event, Option<EventCompletionAction>)>>
941+
) -> Result<[u8; 32], Bolt12PaymentError> where ES::Target: EntropySource {
942+
macro_rules! abandon_with_entry {
943+
($payment: expr, $reason: expr) => {
944+
$payment.get_mut().mark_abandoned($reason);
945+
if let PendingOutboundPayment::Abandoned { reason, .. } = $payment.get() {
946+
if $payment.get().remaining_parts() == 0 {
947+
pending_events.lock().unwrap().push_back((events::Event::PaymentFailed {
948+
payment_id,
949+
payment_hash: None,
950+
reason: *reason,
951+
}, None));
952+
$payment.remove();
953+
}
954+
}
955+
}
956+
}
957+
958+
match self.pending_outbound_payments.lock().unwrap().entry(payment_id) {
959+
hash_map::Entry::Occupied(mut entry) => match entry.get() {
960+
PendingOutboundPayment::AwaitingInvoice {
961+
retry_strategy, retryable_invoice_request, max_total_routing_fee_msat, ..
962+
} => {
963+
let invreq = &retryable_invoice_request
964+
.as_ref()
965+
.ok_or(Bolt12PaymentError::UnexpectedInvoice)?
966+
.invoice_request;
967+
if !invoice.from_same_offer(invreq) {
968+
return Err(Bolt12PaymentError::UnexpectedInvoice)
969+
}
970+
let amount_msat = match InvoiceBuilder::<DerivedSigningPubkey>::amount_msats(invreq) {
971+
Ok(amt) => amt,
972+
Err(_) => {
973+
// We check this during invoice request parsing, when constructing the invreq's
974+
// contents from its TLV stream.
975+
debug_assert!(false, "LDK requires an msat amount in either the invreq or the invreq's underlying offer");
976+
abandon_with_entry!(entry, PaymentFailureReason::UnexpectedError);
977+
return Err(Bolt12PaymentError::UnknownRequiredFeatures)
978+
}
979+
};
980+
let keysend_preimage = PaymentPreimage(entropy_source.get_secure_random_bytes());
981+
let payment_hash = PaymentHash(Sha256::hash(&keysend_preimage.0).to_byte_array());
982+
let payment_release_secret = entropy_source.get_secure_random_bytes();
983+
let pay_params = PaymentParameters::from_static_invoice(invoice);
984+
let mut route_params = RouteParameters::from_payment_params_and_value(pay_params, amount_msat);
985+
route_params.max_total_routing_fee_msat = *max_total_routing_fee_msat;
986+
*entry.into_mut() = PendingOutboundPayment::StaticInvoiceReceived {
987+
payment_hash,
988+
keysend_preimage,
989+
retry_strategy: *retry_strategy,
990+
payment_release_secret,
991+
route_params,
992+
};
993+
return Ok(payment_release_secret)
994+
},
995+
_ => return Err(Bolt12PaymentError::DuplicateInvoice),
996+
},
997+
hash_map::Entry::Vacant(_) => return Err(Bolt12PaymentError::UnexpectedInvoice),
998+
};
999+
}
1000+
9311001
pub(super) fn check_retry_payments<R: Deref, ES: Deref, NS: Deref, SP, IH, FH, L: Deref>(
9321002
&self, router: &R, first_hops: FH, inflight_htlcs: IH, entropy_source: &ES, node_signer: &NS,
9331003
best_block_height: u32,

0 commit comments

Comments
 (0)