lightningd: db migration to clean up any pending payments where theres no htlc.
What changed, and why it matters
This commit adds a database cleanup routine that marks old pending payments as failed if no corresponding payment route (HTLC) actually exists. Previously, payments could be stuck showing as 'pending' forever in commands like listpays/listsendpays, even though nothing was really happening. The fix only corrects stale bookkeeping state; it does not create a way for an attacker to steal funds or force payments to fail.
Treat as a reliability/UX bug fix rather than an urgent security patch. Operators should upgrade normally and verify that stuck pending payments are resolved after restart. Review any automated tooling that may have acted on incorrect pending status.
Security signals we found
Database migration corrects inconsistent payment state
Payments incorrectly left pending could mislead users or downstream automation about real payment status
No cryptographic, network, or authorization weakness introduced by the patch
No evidence of remote exploitability in the diff
Evidence from the diff
A new DB migration, migrate_fail_pending_payments_without_htlcs, runs at startup and UPDATEs payments rows with status=PAYMENT_PENDING to PAYMENT_FAILED when no channel_htlcs row matches the same payment_hash, groupid, and partid. The commit also removes an @pytest.mark.xfail(strict=True) from test_pending_payments_cleanup, confirming the test now passes. The root cause described is that channeld dying or being offline at the wrong moment could leave a payment registered as pending without an HTLC ever being created.
Changed components
wallet/db.clightningd payment state trackingJSON-RPC listpays/listsendpays outputInspect captured patch +25 / −1
diff --git a/tests/test_wallet.py b/tests/test_wallet.py
index f3da6eb4..17cfb928 100644
--- a/tests/test_wallet.py
+++ b/tests/test_wallet.py
@@ -2469,7 +2469,6 @@ def test_old_htlcs_cleanup(node_factory, bitcoind):
assert l1.rpc.listhtlcs() == {'htlcs': []}
-@pytest.mark.xfail(strict=True)
@unittest.skipIf(os.getenv('TEST_DB_PROVIDER', 'sqlite3') != 'sqlite3', "Makes use of the sqlite3 db")
@unittest.skipIf(TEST_NETWORK != 'regtest', "sqlite3 snapshot is regtest")
def test_pending_payments_cleanup(node_factory, bitcoind):
diff --git a/wallet/db.c b/wallet/db.c
index 61f5c28a..0024092b 100644
--- a/wallet/db.c
+++ b/wallet/db.c
@@ -84,6 +84,8 @@ static void migrate_convert_old_channel_keyidx(struct lightningd *ld,
struct db *db);
static void migrate_initialize_channel_htlcs_wait_indexes_and_fixup_forwards(struct lightningd *ld,
struct db *db);
+static void migrate_fail_pending_payments_without_htlcs(struct lightningd *ld,
+ struct db *db);
/* Do not reorder or remove elements from this array, it is used to
* migrate existing databases from a previous state, based on the
@@ -1098,6 +1100,7 @@ static struct migration dbmigrations[] = {
" connect_attempted INTEGER NOT NULL,"
" PRIMARY KEY (id)"
")"), NULL},
+ {NULL, migrate_fail_pending_payments_without_htlcs},
};
/**
@@ -2127,3 +2130,25 @@ static void migrate_convert_old_channel_keyidx(struct lightningd *ld,
db_bind_int(stmt, channel_state_in_db(CLOSED));
db_exec_prepared_v2(take(stmt));
}
+
+static void migrate_fail_pending_payments_without_htlcs(struct lightningd *ld,
+ struct db *db)
+{
+ /* If channeld died or was offline at the right moment, we
+ * could register a payment as pending, but then not create an
+ * HTLC. Clean those up. */
+ struct db_stmt *stmt;
+
+ stmt = db_prepare_v2(db, SQL("UPDATE payments AS p"
+ " SET status = ?"
+ " WHERE p.status = ?"
+ " AND NOT EXISTS ("
+ " SELECT 1"
+ " FROM channel_htlcs AS h"
+ " WHERE h.payment_hash = p.payment_hash"
+ " AND h.groupid = p.groupid"
+ " AND h.partid = p.partid);"));
+ db_bind_int(stmt, payment_status_in_db(PAYMENT_FAILED));
+ db_bind_int(stmt, payment_status_in_db(PAYMENT_PENDING));
+ db_exec_prepared_v2(take(stmt));
+}
Why this scored 30/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.