Problem
ldk-node stores outbound payment records independently from LDK's ChannelManager and ChannelMonitor persistence. That creates two restart-time split-brain cases: LDK can know about an outbound payment that is missing from ldk-node's PaymentStore, or PaymentStore can contain a Pending outbound payment that LDK no longer knows about.
The relevant BOLT11 send path is src/payment/bolt11.rs, Bolt11Payment::send_internal:
- ldk-node derives the local
PaymentId from the invoice payment hash with PaymentId(invoice.payment_hash().0).
- It checks
self.payment_store.get(&payment_id) and blocks only if the existing record is Pending or Succeeded.
- It calls
self.channel_manager.pay_for_bolt11_invoice(invoice, payment_id, amount_msat, optional_params).
- Only after
pay_for_bolt11_invoice returns Ok(()), it constructs a PaymentDetails with PaymentDirection::Outbound and PaymentStatus::Pending.
- It persists that record with
self.runtime.block_on(self.payment_store.insert(payment))?.
Because the LDK payment start and the ldk-node payment-record write are separate persistence streams, a crash or store error around this boundary can leave only one side durable.
On startup, src/builder.rs reads stored payment records with read_all_objects(... PAYMENT_INFO_PERSISTENCE_...) and builds PaymentStore::new(payments, ...). That reconstructs ldk-node's payment view from the ldk-node payment namespace. Node::payment, Node::list_payments, and Node::list_payments_with_filter in src/lib.rs all read from PaymentStore.
I did not find a startup pass that compares ChannelManager::list_recent_payments() against PaymentStore. Without that reconciliation, ldk-node appears to trust whichever side survived in the ldk-node payment namespace and does not repair disagreements with LDK's recent outbound payment state.
Consequence
If LDK knows about a payment but PaymentStore does not, the payment may be in flight or may later complete while remaining invisible to ldk-node payment APIs. This is primarily a payment-history/accounting gap rather than an LDK channel-fund-safety claim. It also feeds into duplicate-payment risk if duplicate protection later depends on a completed record that was never written.
If PaymentStore has a Pending outbound payment but LDK does not, the API can show a permanently stuck pending payment. The same local pending record can also block a retry because Bolt11Payment::send_internal treats Pending records as duplicates before calling LDK.
Related Open Issues
#171 is a partial duplicate. It asks to retry pending payments on restart and points at ChannelManager::list_recent_payments(), so it overlaps with the PaymentStore-pending-but-LDK-missing direction. It does not appear to cover the opposite direction, where LDK has a recent outbound payment but ldk-node has no PaymentStore record, and it does not spell out a general reconciliation contract between the two stores.
Problem
ldk-node stores outbound payment records independently from LDK's
ChannelManagerandChannelMonitorpersistence. That creates two restart-time split-brain cases: LDK can know about an outbound payment that is missing from ldk-node'sPaymentStore, orPaymentStorecan contain aPendingoutbound payment that LDK no longer knows about.The relevant BOLT11 send path is
src/payment/bolt11.rs,Bolt11Payment::send_internal:PaymentIdfrom the invoice payment hash withPaymentId(invoice.payment_hash().0).self.payment_store.get(&payment_id)and blocks only if the existing record isPendingorSucceeded.self.channel_manager.pay_for_bolt11_invoice(invoice, payment_id, amount_msat, optional_params).pay_for_bolt11_invoicereturnsOk(()), it constructs aPaymentDetailswithPaymentDirection::OutboundandPaymentStatus::Pending.self.runtime.block_on(self.payment_store.insert(payment))?.Because the LDK payment start and the ldk-node payment-record write are separate persistence streams, a crash or store error around this boundary can leave only one side durable.
On startup,
src/builder.rsreads stored payment records withread_all_objects(... PAYMENT_INFO_PERSISTENCE_...)and buildsPaymentStore::new(payments, ...). That reconstructs ldk-node's payment view from the ldk-node payment namespace.Node::payment,Node::list_payments, andNode::list_payments_with_filterinsrc/lib.rsall read fromPaymentStore.I did not find a startup pass that compares
ChannelManager::list_recent_payments()againstPaymentStore. Without that reconciliation, ldk-node appears to trust whichever side survived in the ldk-node payment namespace and does not repair disagreements with LDK's recent outbound payment state.Consequence
If LDK knows about a payment but
PaymentStoredoes not, the payment may be in flight or may later complete while remaining invisible to ldk-node payment APIs. This is primarily a payment-history/accounting gap rather than an LDK channel-fund-safety claim. It also feeds into duplicate-payment risk if duplicate protection later depends on a completed record that was never written.If
PaymentStorehas aPendingoutbound payment but LDK does not, the API can show a permanently stuck pending payment. The same local pending record can also block a retry becauseBolt11Payment::send_internaltreatsPendingrecords as duplicates before calling LDK.Related Open Issues
#171 is a partial duplicate. It asks to retry pending payments on restart and points at
ChannelManager::list_recent_payments(), so it overlaps with thePaymentStore-pending-but-LDK-missing direction. It does not appear to cover the opposite direction, where LDK has a recent outbound payment but ldk-node has noPaymentStorerecord, and it does not spell out a general reconciliation contract between the two stores.