Skip to content

Commit ff3345d

Browse files
committed
Track manual BOLT11 invoices
Manual BOLT11 invoices need a pending entry before HTLC arrival. That lets duplicate registrations be rejected while keeping randomized payment IDs. Store the pending entries with their invoice expiry and resolve BOLT11 claimable events by scanning those entries. Co-Authored-By: HAL 9000
1 parent 3f5c23a commit ff3345d

7 files changed

Lines changed: 246 additions & 14 deletions

File tree

src/builder.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2149,6 +2149,7 @@ fn build_with_store_internal(
21492149
scorer,
21502150
peer_store,
21512151
payment_store,
2152+
pending_payment_store,
21522153
lnurl_auth,
21532154
is_running,
21542155
node_metrics,

src/event.rs

Lines changed: 88 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use core::task::{Poll, Waker};
1010
use std::collections::VecDeque;
1111
use std::ops::Deref;
1212
use std::sync::{Arc, Mutex};
13+
use std::time::{Duration, SystemTime, UNIX_EPOCH};
1314

1415
use bitcoin::blockdata::locktime::absolute::LockTime;
1516
use bitcoin::secp256k1::PublicKey;
@@ -53,7 +54,8 @@ use crate::payment::store::{
5354
use crate::payment::PaymentMetadata;
5455
use crate::runtime::Runtime;
5556
use crate::types::{
56-
CustomTlvRecord, DynStore, KeysManager, OnionMessenger, PaymentStore, Sweeper, Wallet,
57+
CustomTlvRecord, DynStore, KeysManager, OnionMessenger, PaymentStore, PendingPaymentStore,
58+
Sweeper, Wallet,
5759
};
5860
use crate::{
5961
hex_utils, BumpTransactionEventHandler, ChannelManager, Error, Graph, PeerInfo, PeerStore,
@@ -529,6 +531,7 @@ where
529531
network_graph: Arc<Graph>,
530532
liquidity_source: Arc<LiquiditySource<Arc<Logger>>>,
531533
payment_store: Arc<PaymentStore>,
534+
pending_payment_store: Arc<PendingPaymentStore>,
532535
peer_store: Arc<PeerStore<L>>,
533536
keys_manager: Arc<KeysManager>,
534537
runtime: Arc<Runtime>,
@@ -549,10 +552,10 @@ where
549552
channel_manager: Arc<ChannelManager>, connection_manager: Arc<ConnectionManager<L>>,
550553
output_sweeper: Arc<Sweeper>, network_graph: Arc<Graph>,
551554
liquidity_source: Arc<LiquiditySource<Arc<Logger>>>, payment_store: Arc<PaymentStore>,
552-
peer_store: Arc<PeerStore<L>>, keys_manager: Arc<KeysManager>,
553-
static_invoice_store: Option<StaticInvoiceStore>, onion_messenger: Arc<OnionMessenger>,
554-
om_mailbox: Option<Arc<OnionMessageMailbox>>, runtime: Arc<Runtime>, logger: L,
555-
config: Arc<Config>,
555+
pending_payment_store: Arc<PendingPaymentStore>, peer_store: Arc<PeerStore<L>>,
556+
keys_manager: Arc<KeysManager>, static_invoice_store: Option<StaticInvoiceStore>,
557+
onion_messenger: Arc<OnionMessenger>, om_mailbox: Option<Arc<OnionMessageMailbox>>,
558+
runtime: Arc<Runtime>, logger: L, config: Arc<Config>,
556559
) -> Self {
557560
Self {
558561
event_queue,
@@ -564,6 +567,7 @@ where
564567
network_graph,
565568
liquidity_source,
566569
payment_store,
570+
pending_payment_store,
567571
peer_store,
568572
keys_manager,
569573
logger,
@@ -605,6 +609,59 @@ where
605609
})
606610
}
607611

612+
fn current_time_secs() -> u64 {
613+
SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or(Duration::from_secs(0)).as_secs()
614+
}
615+
616+
async fn prune_expired_pending_payments(&self) -> Result<(), ReplayEvent> {
617+
let now = Self::current_time_secs();
618+
let expired_payment_ids = self
619+
.pending_payment_store
620+
.list_filter(|payment| payment.has_expired(now))
621+
.into_iter()
622+
.map(|payment| payment.details.id)
623+
.collect::<Vec<_>>();
624+
625+
for payment_id in expired_payment_ids {
626+
if let Err(e) = self.pending_payment_store.remove(&payment_id).await {
627+
log_error!(
628+
self.logger,
629+
"Failed to remove expired pending payment with ID {}: {}",
630+
payment_id,
631+
e
632+
);
633+
return Err(ReplayEvent());
634+
}
635+
}
636+
637+
Ok(())
638+
}
639+
640+
fn find_inbound_payment_by_hash(&self, payment_hash: &PaymentHash) -> Option<PaymentDetails> {
641+
self.payment_store
642+
.list_filter(|payment| {
643+
payment.direction == PaymentDirection::Inbound
644+
&& matches!(&payment.kind, PaymentKind::Bolt11 { hash, .. } if hash == payment_hash)
645+
})
646+
.first()
647+
.cloned()
648+
}
649+
650+
async fn find_pending_inbound_payment_id(
651+
&self, payment_hash: &PaymentHash,
652+
) -> Result<Option<PaymentId>, ReplayEvent> {
653+
self.prune_expired_pending_payments().await?;
654+
Ok(self
655+
.pending_payment_store
656+
.list_filter(|payment| {
657+
payment.details.direction == PaymentDirection::Inbound
658+
&& payment.details.status == PaymentStatus::Pending
659+
&& matches!(&payment.details.kind, PaymentKind::Bolt11 { hash, .. } if hash == payment_hash)
660+
})
661+
.first()
662+
.map(|payment| payment.details.id))
663+
}
664+
608665
fn resolve_inbound_payment_id(
609666
&self, event_payment_id: PaymentId, payment_hash: &PaymentHash,
610667
) -> (PaymentId, Option<PaymentDetails>) {
@@ -619,6 +676,10 @@ where
619676
}
620677
}
621678

679+
if let Some(info) = self.find_inbound_payment_by_hash(payment_hash) {
680+
return (info.id, Some(info));
681+
}
682+
622683
(event_payment_id, None)
623684
}
624685

@@ -735,8 +796,16 @@ where
735796
..
736797
} => {
737798
let event_payment_id = payment_id.unwrap_or(PaymentId(payment_hash.0));
738-
let (payment_id, payment_info) =
799+
let (mut payment_id, payment_info) =
739800
self.resolve_inbound_payment_id(event_payment_id, &payment_hash);
801+
let pending_payment_id = if payment_info.is_none() {
802+
self.find_pending_inbound_payment_id(&payment_hash).await?
803+
} else {
804+
None
805+
};
806+
if let Some(pending_payment_id) = pending_payment_id {
807+
payment_id = pending_payment_id;
808+
}
740809
if let Some(info) = payment_info.as_ref() {
741810
if info.direction == PaymentDirection::Outbound {
742811
log_info!(
@@ -911,6 +980,19 @@ where
911980
return Err(ReplayEvent());
912981
},
913982
}
983+
984+
if pending_payment_id.is_some() {
985+
if let Err(e) = self.pending_payment_store.remove(&payment_id).await
986+
{
987+
log_error!(
988+
self.logger,
989+
"Failed to remove pending payment with ID {}: {}",
990+
payment_id,
991+
e
992+
);
993+
return Err(ReplayEvent());
994+
}
995+
}
914996
}
915997

916998
if payment_preimage.is_none() {

src/lib.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -176,8 +176,8 @@ use runtime::Runtime;
176176
pub use tokio;
177177
use types::{
178178
Broadcaster, BumpTransactionEventHandler, ChainMonitor, ChannelManager, DynStore, Graph,
179-
HRNResolver, KeysManager, OnionMessenger, PaymentStore, PeerManager, Router, Scorer, Sweeper,
180-
Wallet,
179+
HRNResolver, KeysManager, OnionMessenger, PaymentStore, PeerManager, PendingPaymentStore,
180+
Router, Scorer, Sweeper, Wallet,
181181
};
182182
pub use types::{
183183
ChannelCounterparty, ChannelDetails, CustomTlvRecord, PeerDetails, ReserveType, UserChannelId,
@@ -244,6 +244,7 @@ pub struct Node {
244244
scorer: Arc<Mutex<Scorer>>,
245245
peer_store: Arc<PeerStore<Arc<Logger>>>,
246246
payment_store: Arc<PaymentStore>,
247+
pending_payment_store: Arc<PendingPaymentStore>,
247248
lnurl_auth: Arc<LnurlAuth>,
248249
is_running: Arc<RwLock<bool>>,
249250
node_metrics: Arc<PersistedNodeMetrics>,
@@ -605,6 +606,7 @@ impl Node {
605606
Arc::clone(&self.network_graph),
606607
Arc::clone(&self.liquidity_source),
607608
Arc::clone(&self.payment_store),
609+
Arc::clone(&self.pending_payment_store),
608610
Arc::clone(&self.peer_store),
609611
Arc::clone(&self.keys_manager),
610612
static_invoice_store,
@@ -949,6 +951,7 @@ impl Node {
949951
Arc::clone(&self.connection_manager),
950952
Arc::clone(&self.liquidity_source),
951953
Arc::clone(&self.payment_store),
954+
Arc::clone(&self.pending_payment_store),
952955
Arc::clone(&self.peer_store),
953956
Arc::clone(&self.config),
954957
Arc::clone(&self.is_running),
@@ -968,6 +971,7 @@ impl Node {
968971
Arc::clone(&self.connection_manager),
969972
Arc::clone(&self.liquidity_source),
970973
Arc::clone(&self.payment_store),
974+
Arc::clone(&self.pending_payment_store),
971975
Arc::clone(&self.peer_store),
972976
Arc::clone(&self.config),
973977
Arc::clone(&self.is_running),

src/payment/bolt11.rs

Lines changed: 110 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
//! [BOLT 11]: https://github.com/lightning/bolts/blob/master/11-payment-encoding.md
1111
1212
use std::sync::{Arc, RwLock};
13+
use std::time::{Duration, SystemTime, UNIX_EPOCH};
1314

1415
use bitcoin::hashes::sha256::Hash as Sha256;
1516
use bitcoin::hashes::Hash;
@@ -23,7 +24,7 @@ use lightning::sign::EntropySource;
2324
use lightning_invoice::{
2425
Bolt11Invoice as LdkBolt11Invoice, Bolt11InvoiceDescription as LdkBolt11InvoiceDescription,
2526
};
26-
use lightning_types::payment::{PaymentHash, PaymentPreimage};
27+
use lightning_types::payment::{PaymentHash, PaymentPreimage, PaymentSecret};
2728

2829
use crate::config::{Config, LDK_PAYMENT_RETRY_TIMEOUT};
2930
use crate::connection::ConnectionManager;
@@ -36,9 +37,10 @@ use crate::payment::store::{
3637
LSPS2Parameters, PaymentDetails, PaymentDetailsUpdate, PaymentDirection, PaymentKind,
3738
PaymentStatus,
3839
};
40+
use crate::payment::PendingPaymentDetails;
3941
use crate::peer_store::{PeerInfo, PeerStore};
4042
use crate::runtime::Runtime;
41-
use crate::types::{ChannelManager, KeysManager, PaymentStore};
43+
use crate::types::{ChannelManager, KeysManager, PaymentStore, PendingPaymentStore};
4244

4345
#[cfg(not(feature = "uniffi"))]
4446
type Bolt11Invoice = LdkBolt11Invoice;
@@ -74,6 +76,7 @@ pub struct Bolt11Payment {
7476
connection_manager: Arc<ConnectionManager<Arc<Logger>>>,
7577
liquidity_source: Arc<LiquiditySource<Arc<Logger>>>,
7678
payment_store: Arc<PaymentStore>,
79+
pending_payment_store: Arc<PendingPaymentStore>,
7780
peer_store: Arc<PeerStore<Arc<Logger>>>,
7881
config: Arc<Config>,
7982
is_running: Arc<RwLock<bool>>,
@@ -85,8 +88,8 @@ impl Bolt11Payment {
8588
runtime: Arc<Runtime>, channel_manager: Arc<ChannelManager>,
8689
keys_manager: Arc<KeysManager>, connection_manager: Arc<ConnectionManager<Arc<Logger>>>,
8790
liquidity_source: Arc<LiquiditySource<Arc<Logger>>>, payment_store: Arc<PaymentStore>,
88-
peer_store: Arc<PeerStore<Arc<Logger>>>, config: Arc<Config>,
89-
is_running: Arc<RwLock<bool>>, logger: Arc<Logger>,
91+
pending_payment_store: Arc<PendingPaymentStore>, peer_store: Arc<PeerStore<Arc<Logger>>>,
92+
config: Arc<Config>, is_running: Arc<RwLock<bool>>, logger: Arc<Logger>,
9093
) -> Self {
9194
Self {
9295
runtime,
@@ -95,17 +98,94 @@ impl Bolt11Payment {
9598
connection_manager,
9699
liquidity_source,
97100
payment_store,
101+
pending_payment_store,
98102
peer_store,
99103
config,
100104
is_running,
101105
logger,
102106
}
103107
}
104108

109+
fn current_time_secs() -> u64 {
110+
SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or(Duration::from_secs(0)).as_secs()
111+
}
112+
113+
fn prune_expired_pending_payments(&self) -> Result<(), Error> {
114+
let now = Self::current_time_secs();
115+
let expired_payment_ids = self
116+
.pending_payment_store
117+
.list_filter(|payment| payment.has_expired(now))
118+
.into_iter()
119+
.map(|payment| payment.details.id)
120+
.collect::<Vec<_>>();
121+
122+
for payment_id in expired_payment_ids {
123+
self.runtime.block_on(self.pending_payment_store.remove(&payment_id))?;
124+
}
125+
126+
Ok(())
127+
}
128+
129+
fn has_pending_or_succeeded_inbound_payment(&self, payment_hash: &PaymentHash) -> bool {
130+
!self
131+
.payment_store
132+
.list_filter(|payment| {
133+
payment.direction == PaymentDirection::Inbound
134+
&& matches!(&payment.kind, PaymentKind::Bolt11 { hash, .. } if hash == payment_hash)
135+
&& matches!(payment.status, PaymentStatus::Pending | PaymentStatus::Succeeded)
136+
})
137+
.is_empty()
138+
|| !self
139+
.pending_payment_store
140+
.list_filter(|payment| {
141+
payment.details.direction == PaymentDirection::Inbound
142+
&& matches!(&payment.details.kind, PaymentKind::Bolt11 { hash, .. } if hash == payment_hash)
143+
&& matches!(
144+
payment.details.status,
145+
PaymentStatus::Pending | PaymentStatus::Succeeded
146+
)
147+
})
148+
.is_empty()
149+
}
150+
151+
fn register_manual_claim_invoice(
152+
&self, payment_hash: PaymentHash, amount_msat: Option<u64>, payment_secret: PaymentSecret,
153+
expiry_secs: u32,
154+
) -> Result<(), Error> {
155+
let payment_id = PaymentId(self.keys_manager.get_secure_random_bytes());
156+
let kind = PaymentKind::Bolt11 {
157+
hash: payment_hash,
158+
preimage: None,
159+
secret: Some(payment_secret),
160+
counterparty_skimmed_fee_msat: None,
161+
};
162+
let payment = PaymentDetails::new(
163+
payment_id,
164+
kind,
165+
amount_msat,
166+
None,
167+
PaymentDirection::Inbound,
168+
PaymentStatus::Pending,
169+
);
170+
let expires_at = Some(Self::current_time_secs().saturating_add(expiry_secs as u64));
171+
let pending_payment =
172+
PendingPaymentDetails::new_with_expiry(payment, Vec::new(), expires_at);
173+
self.runtime.block_on(self.pending_payment_store.insert_or_update(pending_payment))?;
174+
Ok(())
175+
}
176+
105177
pub(crate) fn receive_inner(
106178
&self, amount_msat: Option<u64>, invoice_description: &LdkBolt11InvoiceDescription,
107179
expiry_secs: u32, manual_claim_payment_hash: Option<PaymentHash>,
108180
) -> Result<LdkBolt11Invoice, Error> {
181+
if let Some(payment_hash) = manual_claim_payment_hash {
182+
self.prune_expired_pending_payments()?;
183+
if self.has_pending_or_succeeded_inbound_payment(&payment_hash) {
184+
log_error!(self.logger, "Payment error: an invoice must not be paid twice.");
185+
return Err(Error::DuplicatePayment);
186+
}
187+
}
188+
109189
let invoice = {
110190
let invoice_params = Bolt11InvoiceParameters {
111191
amount_msats: amount_msat,
@@ -127,6 +207,15 @@ impl Bolt11Payment {
127207
}
128208
};
129209

210+
if let Some(payment_hash) = manual_claim_payment_hash {
211+
self.register_manual_claim_invoice(
212+
payment_hash,
213+
amount_msat,
214+
*invoice.payment_secret(),
215+
expiry_secs,
216+
)?;
217+
}
218+
130219
Ok(invoice)
131220
}
132221

@@ -135,6 +224,14 @@ impl Bolt11Payment {
135224
expiry_secs: u32, max_total_lsp_fee_limit_msat: Option<u64>,
136225
max_proportional_lsp_fee_limit_ppm_msat: Option<u64>, payment_hash: Option<PaymentHash>,
137226
) -> Result<LdkBolt11Invoice, Error> {
227+
if let Some(payment_hash) = payment_hash {
228+
self.prune_expired_pending_payments()?;
229+
if self.has_pending_or_succeeded_inbound_payment(&payment_hash) {
230+
log_error!(self.logger, "Payment error: an invoice must not be paid twice.");
231+
return Err(Error::DuplicatePayment);
232+
}
233+
}
234+
138235
let connection_manager = Arc::clone(&self.connection_manager);
139236
let (invoice, chosen_lsp) = self.runtime.block_on(async move {
140237
if let Some(amount_msat) = amount_msat {
@@ -167,6 +264,15 @@ impl Bolt11Payment {
167264
let peer_info = PeerInfo { node_id: chosen_lsp.node_id, address: chosen_lsp.address };
168265
self.runtime.block_on(self.peer_store.add_peer(peer_info))?;
169266

267+
if let Some(payment_hash) = payment_hash {
268+
self.register_manual_claim_invoice(
269+
payment_hash,
270+
amount_msat,
271+
*invoice.payment_secret(),
272+
expiry_secs,
273+
)?;
274+
}
275+
170276
Ok(invoice)
171277
}
172278
}

0 commit comments

Comments
 (0)