Skip to content

Commit 7ad18fa

Browse files
committed
Fix BOLT11 annotation loss after restart
Fixes #6978 where BOLT11 invoice annotations were lost after lightning node restart due to broken linkage between HTLCs and payment records. Add payment_id column to channel_htlcs table with proper foreign key relationship to maintain HTLC-payment linkage through restarts.
1 parent 2e2a085 commit 7ad18fa

File tree

3 files changed

+161
-2
lines changed

3 files changed

+161
-2
lines changed

tests/test_pay.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7048,3 +7048,109 @@ def test_htlc_tlv_crash(node_factory):
70487048

70497049
l1.rpc.waitsendpay(inv1['payment_hash'], TIMEOUT)
70507050
l1.rpc.waitsendpay(inv2['payment_hash'], TIMEOUT)
7051+
7052+
7053+
@pytest.mark.openchannel('v1')
7054+
@pytest.mark.openchannel('v2')
7055+
def test_bolt11_annotation_persistence(node_factory):
7056+
"""Test that HTLCs maintain proper linkage to payment records with BOLT11."""
7057+
l1, l2 = node_factory.line_graph(2)
7058+
7059+
inv = l2.rpc.invoice(100000, 'test_annotation', 'Test Description')['bolt11']
7060+
l1.dev_pay(inv, dev_use_shadow=False)
7061+
7062+
payments = l1.rpc.listpays()['pays']
7063+
payment = [p for p in payments if p['bolt11'] == inv][0]
7064+
payment_hash = payment['payment_hash']
7065+
7066+
# Verify HTLCs have payment_id references
7067+
htlcs = l1.db_query(f"""
7068+
SELECT payment_id, partid, groupid
7069+
FROM channel_htlcs
7070+
WHERE payment_hash = x'{payment_hash}'
7071+
""")
7072+
assert len(htlcs) > 0
7073+
7074+
# Get payment records for verification
7075+
payment_records = l1.db_query(f"""
7076+
SELECT id, partid, groupid
7077+
FROM payments
7078+
WHERE payment_hash = x'{payment_hash}'
7079+
""")
7080+
assert len(payment_records) > 0
7081+
7082+
# Verify each HTLC links to correct payment record
7083+
for htlc in htlcs:
7084+
assert htlc['payment_id'] is not None
7085+
7086+
# Find matching payment record
7087+
matching_payments = [p for p in payment_records
7088+
if p['id'] == htlc['payment_id']
7089+
and p['partid'] == htlc['partid']
7090+
and p['groupid'] == htlc['groupid']]
7091+
assert len(matching_payments) == 1
7092+
7093+
# Verify payment_id index exists for performance
7094+
indexes = l1.db_query("""
7095+
SELECT name FROM sqlite_master
7096+
WHERE type = 'index' AND name = 'channel_htlcs_payment_id_idx'
7097+
""")
7098+
assert len(indexes) == 1
7099+
7100+
l1.restart()
7101+
7102+
# After restart, verify BOLT11 annotation persistence
7103+
payments_after = l1.rpc.listpays()['pays']
7104+
payment_after = [p for p in payments_after if p['bolt11'] == inv]
7105+
assert len(payment_after) == 1
7106+
7107+
# Verify payment data integrity
7108+
assert payment_after[0]['payment_hash'] == payment_hash
7109+
assert payment_after[0]['status'] == 'complete'
7110+
7111+
# Verify HTLCs still have payment_id linkage after restart
7112+
htlcs_after = l1.db_query(f"""
7113+
SELECT payment_id, partid, groupid
7114+
FROM channel_htlcs
7115+
WHERE payment_hash = x'{payment_hash}'
7116+
""")
7117+
assert len(htlcs_after) == len(htlcs)
7118+
7119+
for htlc in htlcs_after:
7120+
assert htlc['payment_id'] is not None
7121+
7122+
# Re-verify linkage integrity after restart
7123+
payment_check = l1.db_query(f"""
7124+
SELECT COUNT(*) as count
7125+
FROM payments
7126+
WHERE id = {htlc['payment_id']}
7127+
AND partid = {htlc['partid']}
7128+
AND groupid = {htlc['groupid']}
7129+
""")
7130+
assert payment_check[0]['count'] == 1
7131+
7132+
7133+
@pytest.mark.openchannel('v1')
7134+
@pytest.mark.openchannel('v2')
7135+
def test_bolt11_payment_id_migration(node_factory):
7136+
"""Test that the migration properly adds payment_id column to channel_htlcs."""
7137+
l1, l2 = node_factory.line_graph(2)
7138+
7139+
# Create some payments to test migration worked
7140+
inv1 = l2.rpc.invoice(100000, 'pre_migration_1', 'Test Description')['bolt11']
7141+
inv2 = l2.rpc.invoice(200000, 'pre_migration_2', 'Test Description 2')['bolt11']
7142+
7143+
l1.dev_pay(inv1, dev_use_shadow=False)
7144+
l1.dev_pay(inv2, dev_use_shadow=False)
7145+
7146+
# Verify payment_id column exists in schema
7147+
schema = l1.db_query("SELECT sql FROM sqlite_master WHERE name = 'channel_htlcs'")
7148+
assert any('payment_id' in row['sql'] for row in schema)
7149+
7150+
# Verify index exists
7151+
indexes = l1.db_query("SELECT name FROM sqlite_master WHERE type = 'index' AND name = 'channel_htlcs_payment_id_idx'")
7152+
assert len(indexes) == 1
7153+
7154+
# Verify HTLCs have proper linkage
7155+
htlcs = l1.db_query("SELECT payment_id FROM channel_htlcs WHERE payment_id IS NOT NULL")
7156+
assert len(htlcs) >= 2

wallet/db.c

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ static void migrate_our_funding(struct lightningd *ld, struct db *db);
3535

3636
static void migrate_last_tx_to_psbt(struct lightningd *ld, struct db *db);
3737

38+
static void migrate_add_payment_id_to_htlcs(struct lightningd *ld, struct db *db);
39+
3840
static void
3941
migrate_inflight_last_tx_to_psbt(struct lightningd *ld, struct db *db);
4042

@@ -1092,6 +1094,8 @@ static struct migration dbmigrations[] = {
10921094
")"), NULL},
10931095
/* We do a lookup before each append, to avoid duplicates */
10941096
{SQL("CREATE INDEX chain_moves_utxo_idx ON chain_moves (utxo)"), NULL},
1097+
/* Add payment_id column to channel_htlcs */
1098+
{NULL, migrate_add_payment_id_to_htlcs},
10951099
{NULL, migrate_from_account_db},
10961100
};
10971101

@@ -2080,6 +2084,31 @@ static void migrate_initialize_alias_local(struct lightningd *ld,
20802084
}
20812085
}
20822086

2087+
/* Add payment_id column to channel_htlcs */
2088+
static void migrate_add_payment_id_to_htlcs(struct lightningd *ld, struct db *db)
2089+
{
2090+
struct db_stmt *stmt;
2091+
2092+
stmt = db_prepare_v2(db, SQL("ALTER TABLE channel_htlcs ADD COLUMN payment_id BIGINT"));
2093+
db_exec_prepared_v2(stmt);
2094+
tal_free(stmt);
2095+
2096+
stmt = db_prepare_v2(db, SQL("UPDATE channel_htlcs "
2097+
"SET payment_id = ("
2098+
" SELECT p.id FROM payments p "
2099+
" WHERE p.payment_hash = channel_htlcs.payment_hash "
2100+
" AND p.partid = channel_htlcs.partid "
2101+
" AND p.groupid = channel_htlcs.groupid "
2102+
" LIMIT 1"
2103+
")"));
2104+
db_exec_prepared_v2(stmt);
2105+
tal_free(stmt);
2106+
2107+
stmt = db_prepare_v2(db, SQL("CREATE INDEX channel_htlcs_payment_id_idx ON channel_htlcs (payment_id)"));
2108+
db_exec_prepared_v2(stmt);
2109+
tal_free(stmt);
2110+
}
2111+
20832112
/* Insert address type as `ADDR_ALL` for issued addresses */
20842113
static void insert_addrtype_to_addresses(struct lightningd *ld,
20852114
struct db *db)

wallet/wallet.c

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3316,11 +3316,29 @@ void wallet_htlc_save_out(struct wallet *wallet,
33163316
struct htlc_out *out)
33173317
{
33183318
struct db_stmt *stmt;
3319+
u64 payment_id = 0;
33193320

33203321
/* We absolutely need the incoming HTLC to be persisted before
33213322
* we can persist it's dependent */
33223323
assert(out->in == NULL || out->in->dbid != 0);
33233324

3325+
/* Get payment_id if this is an outgoing payment */
3326+
if (out->am_origin) {
3327+
struct db_stmt *payment_stmt = db_prepare_v2(wallet->db,
3328+
SQL("SELECT id FROM payments "
3329+
"WHERE payment_hash = ? AND partid = ? AND groupid = ? "
3330+
"LIMIT 1"));
3331+
db_bind_sha256(payment_stmt, &out->payment_hash);
3332+
db_bind_u64(payment_stmt, out->partid);
3333+
db_bind_u64(payment_stmt, out->groupid);
3334+
db_query_prepared(payment_stmt);
3335+
3336+
if (db_step(payment_stmt)) {
3337+
payment_id = db_col_u64(payment_stmt, "id");
3338+
}
3339+
tal_free(payment_stmt);
3340+
}
3341+
33243342
stmt = db_prepare_v2(
33253343
wallet->db,
33263344
SQL("INSERT INTO channel_htlcs ("
@@ -3339,8 +3357,9 @@ void wallet_htlc_save_out(struct wallet *wallet,
33393357
" partid,"
33403358
" groupid,"
33413359
" fees_msat,"
3342-
" min_commit_num"
3343-
") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?);"));
3360+
" min_commit_num,"
3361+
" payment_id"
3362+
") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?, ?);"));
33443363

33453364
out->dbid = htlcs_index_created(wallet->ld,
33463365
out->key.id,
@@ -3384,6 +3403,11 @@ void wallet_htlc_save_out(struct wallet *wallet,
33843403
db_bind_u64(stmt, min_u64(chan->next_index[LOCAL]-1,
33853404
chan->next_index[REMOTE]-1));
33863405

3406+
if (payment_id > 0)
3407+
db_bind_u64(stmt, payment_id);
3408+
else
3409+
db_bind_null(stmt);
3410+
33873411
db_exec_prepared_v2(take(stmt));
33883412
}
33893413

0 commit comments

Comments
 (0)