Skip to content

Commit da39b52

Browse files
committed
f Reserve manual invoice hashes first
Reject duplicate manual BOLT11 hashes from the pending store before creating the invoice. This keeps duplicate detection and registration serialized, while preserving the persist-before-index ordering for crash recovery. Co-Authored-By: HAL 9000
1 parent 4a62c03 commit da39b52

2 files changed

Lines changed: 135 additions & 19 deletions

File tree

src/payment/bolt11.rs

Lines changed: 53 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -123,19 +123,15 @@ impl Bolt11Payment {
123123
Ok(())
124124
}
125125

126-
fn has_pending_inbound_payment(&self, payment_hash: &PaymentHash) -> bool {
127-
self.pending_payment_store.get_pending_manual_bolt11_by_payment_hash(payment_hash).is_some()
128-
}
129-
130-
fn register_manual_claim_invoice(
131-
&self, payment_hash: PaymentHash, amount_msat: Option<u64>, payment_secret: PaymentSecret,
126+
fn pending_manual_claim_invoice(
127+
payment_hash: PaymentHash, amount_msat: Option<u64>, payment_secret: Option<PaymentSecret>,
132128
expiry_secs: u32,
133-
) -> Result<(), Error> {
129+
) -> PendingPaymentDetails {
134130
let payment_id = PaymentId(payment_hash.0);
135131
let kind = PaymentKind::Bolt11 {
136132
hash: payment_hash,
137133
preimage: None,
138-
secret: Some(payment_secret),
134+
secret: payment_secret,
139135
counterparty_skimmed_fee_msat: None,
140136
};
141137
let payment = PaymentDetails::new(
@@ -147,22 +143,51 @@ impl Bolt11Payment {
147143
PaymentStatus::Pending,
148144
);
149145
let expires_at = Some(Self::current_time_secs().saturating_add(expiry_secs as u64));
146+
PendingPaymentDetails::new_with_expiry(payment, Vec::new(), expires_at)
147+
}
148+
149+
fn reserve_manual_claim_invoice(
150+
&self, payment_hash: PaymentHash, amount_msat: Option<u64>, expiry_secs: u32,
151+
) -> Result<(), Error> {
150152
let pending_payment =
151-
PendingPaymentDetails::new_with_expiry(payment, Vec::new(), expires_at);
153+
Self::pending_manual_claim_invoice(payment_hash, amount_msat, None, expiry_secs);
154+
if let Err(e) =
155+
self.runtime.block_on(self.pending_payment_store.insert_manual_bolt11(pending_payment))
156+
{
157+
if e == Error::DuplicatePayment {
158+
log_error!(self.logger, "Payment error: an invoice must not be paid twice.");
159+
}
160+
return Err(e);
161+
}
162+
Ok(())
163+
}
164+
165+
fn register_manual_claim_invoice(
166+
&self, payment_hash: PaymentHash, amount_msat: Option<u64>, payment_secret: PaymentSecret,
167+
expiry_secs: u32,
168+
) -> Result<(), Error> {
169+
let pending_payment = Self::pending_manual_claim_invoice(
170+
payment_hash,
171+
amount_msat,
172+
Some(payment_secret),
173+
expiry_secs,
174+
);
152175
self.runtime.block_on(self.pending_payment_store.insert_or_update(pending_payment))?;
153176
Ok(())
154177
}
155178

179+
fn remove_manual_claim_invoice(&self, payment_hash: PaymentHash) -> Result<(), Error> {
180+
let payment_id = PaymentId(payment_hash.0);
181+
self.runtime.block_on(self.pending_payment_store.remove(&payment_id))
182+
}
183+
156184
pub(crate) fn receive_inner(
157185
&self, amount_msat: Option<u64>, invoice_description: &LdkBolt11InvoiceDescription,
158186
expiry_secs: u32, manual_claim_payment_hash: Option<PaymentHash>,
159187
) -> Result<LdkBolt11Invoice, Error> {
160188
if let Some(payment_hash) = manual_claim_payment_hash {
161189
self.prune_expired_pending_payments()?;
162-
if self.has_pending_inbound_payment(&payment_hash) {
163-
log_error!(self.logger, "Payment error: an invoice must not be paid twice.");
164-
return Err(Error::DuplicatePayment);
165-
}
190+
self.reserve_manual_claim_invoice(payment_hash, amount_msat, expiry_secs)?;
166191
}
167192

168193
let invoice = {
@@ -181,6 +206,9 @@ impl Bolt11Payment {
181206
},
182207
Err(e) => {
183208
log_error!(self.logger, "Failed to create invoice: {}", e);
209+
if let Some(payment_hash) = manual_claim_payment_hash {
210+
self.remove_manual_claim_invoice(payment_hash)?;
211+
}
184212
return Err(Error::InvoiceCreationFailed);
185213
},
186214
}
@@ -234,14 +262,11 @@ impl Bolt11Payment {
234262
) -> Result<LdkBolt11Invoice, Error> {
235263
if let Some(payment_hash) = payment_hash {
236264
self.prune_expired_pending_payments()?;
237-
if self.has_pending_inbound_payment(&payment_hash) {
238-
log_error!(self.logger, "Payment error: an invoice must not be paid twice.");
239-
return Err(Error::DuplicatePayment);
240-
}
265+
self.reserve_manual_claim_invoice(payment_hash, amount_msat, expiry_secs)?;
241266
}
242267

243268
let connection_manager = Arc::clone(&self.connection_manager);
244-
let (invoice, chosen_lsp) = self.runtime.block_on(async move {
269+
let res = self.runtime.block_on(async move {
245270
if let Some(amount_msat) = amount_msat {
246271
self.liquidity_source
247272
.lsps2_client()
@@ -266,7 +291,16 @@ impl Bolt11Payment {
266291
)
267292
.await
268293
}
269-
})?;
294+
});
295+
let (invoice, chosen_lsp) = match res {
296+
Ok(res) => res,
297+
Err(e) => {
298+
if let Some(payment_hash) = payment_hash {
299+
self.remove_manual_claim_invoice(payment_hash)?;
300+
}
301+
return Err(e);
302+
},
303+
};
270304

271305
if let Some(payment_hash) = payment_hash {
272306
self.register_manual_claim_invoice(

src/payment/pending_payment_store.rs

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,33 @@ where
229229
&self, pending_payment: PendingPaymentDetails,
230230
) -> Result<bool, Error> {
231231
let _guard = self.mutation_lock.lock().await;
232+
self.insert_or_update_locked(pending_payment).await
233+
}
234+
235+
pub(crate) async fn insert_manual_bolt11(
236+
&self, pending_payment: PendingPaymentDetails,
237+
) -> Result<(), Error> {
238+
let _guard = self.mutation_lock.lock().await;
239+
let Some(payment_hash) = manual_bolt11_payment_hash(&pending_payment.details) else {
240+
debug_assert!(false, "manual BOLT11 insert requires a pending inbound BOLT11 payment");
241+
self.insert_or_update_locked(pending_payment).await?;
242+
return Ok(());
243+
};
244+
let duplicate = {
245+
let index = self.manual_bolt11_payment_hash_index.lock().expect("lock");
246+
index.get(&payment_hash).map_or(false, |ids| !ids.is_empty())
247+
};
248+
if duplicate {
249+
return Err(Error::DuplicatePayment);
250+
}
251+
252+
self.insert_or_update_locked(pending_payment).await?;
253+
Ok(())
254+
}
255+
256+
async fn insert_or_update_locked(
257+
&self, pending_payment: PendingPaymentDetails,
258+
) -> Result<bool, Error> {
232259
let id = pending_payment.id();
233260
let before = self.inner.get(&id);
234261
let updated = self.inner.insert_or_update(pending_payment).await?;
@@ -342,10 +369,14 @@ fn manual_bolt11_payment_hash(payment: &PaymentDetails) -> Option<PaymentHash> {
342369
#[cfg(test)]
343370
mod tests {
344371
use bitcoin::hashes::Hash;
372+
use lightning::util::test_utils::TestLogger;
373+
use lightning_types::payment::PaymentSecret;
345374

346375
use super::*;
376+
use crate::io::test_utils::InMemoryStore;
347377
use crate::payment::store::ConfirmationStatus;
348378
use crate::payment::{PaymentDirection, PaymentKind, PaymentStatus};
379+
use crate::types::{DynStore, DynStoreWrapper};
349380

350381
#[test]
351382
fn pending_payment_candidate_lookup() {
@@ -414,6 +445,57 @@ mod tests {
414445
)
415446
}
416447

448+
fn pending_store() -> PendingPaymentStore<Arc<TestLogger>> {
449+
let store: Arc<DynStore> = Arc::new(DynStoreWrapper(InMemoryStore::new()));
450+
let logger = Arc::new(TestLogger::new());
451+
PendingPaymentStore::new(
452+
Vec::new(),
453+
"pending_payment_store_test_primary".to_string(),
454+
"pending_payment_store_test_secondary".to_string(),
455+
store,
456+
logger,
457+
)
458+
}
459+
460+
fn pending_manual_bolt11_payment(
461+
payment_hash: PaymentHash, payment_secret: Option<PaymentSecret>,
462+
) -> PendingPaymentDetails {
463+
let payment_id = PaymentId(payment_hash.0);
464+
let details = PaymentDetails::new(
465+
payment_id,
466+
PaymentKind::Bolt11 {
467+
hash: payment_hash,
468+
preimage: None,
469+
secret: payment_secret,
470+
counterparty_skimmed_fee_msat: None,
471+
},
472+
Some(1_000),
473+
None,
474+
PaymentDirection::Inbound,
475+
PaymentStatus::Pending,
476+
);
477+
PendingPaymentDetails::new_with_expiry(details, Vec::new(), Some(1_000_000))
478+
}
479+
480+
#[tokio::test]
481+
async fn manual_bolt11_insert_rejects_duplicate_hash() {
482+
let store = pending_store();
483+
let payment_hash = PaymentHash([42; 32]);
484+
let pending_payment = pending_manual_bolt11_payment(payment_hash, None);
485+
assert_eq!(store.insert_manual_bolt11(pending_payment.clone()).await, Ok(()));
486+
487+
let duplicate = pending_manual_bolt11_payment(payment_hash, None);
488+
assert_eq!(store.insert_manual_bolt11(duplicate).await, Err(Error::DuplicatePayment));
489+
assert_eq!(
490+
store.get_pending_manual_bolt11_by_payment_hash(&payment_hash),
491+
Some(pending_payment)
492+
);
493+
494+
let updated = pending_manual_bolt11_payment(payment_hash, Some(PaymentSecret([43; 32])));
495+
assert_eq!(store.insert_or_update(updated.clone()).await, Ok(true));
496+
assert_eq!(store.get_pending_manual_bolt11_by_payment_hash(&payment_hash), Some(updated));
497+
}
498+
417499
#[test]
418500
fn pending_onchain_conflicts_exclude_current_txid_after_txid_rotation() {
419501
let original_txid = test_txid(1);

0 commit comments

Comments
 (0)