wallet: Update the max_index cache when issuing new addresses
What changed, and why it matters
This commit fixes a bug where Core Lightning's in-memory record of the highest address index could fall out of step with the database. When a user generated a new on-chain address, the code updated the database directly but left the cached value unchanged. That meant newly created addresses would not show up in address lists or rescan windows until the node was restarted, and in some cases the stale cache could overwrite the newer database value with an older one. The patch routes all reads and writes through helper functions so the cache and database stay synchronized.
Apply the patch. Nodes that have already generated addresses while running affected code should be restarted after upgrading so the cache is rebuilt from the database at startup, and operators should verify that expected addresses appear in `listaddresses` and on-chain rescans.
Security signals we found
Cache/database inconsistency in address index tracking
Potential for stale lower index to overwrite newer persisted index
Freshly generated addresses invisible to wallet rescan/listing until restart
Could cause missed on-chain funds or incorrect address gap handling
Evidence from the diff
The wallet keeps bip32_max_index and bip86_max_index as in-memory caches of database intvars. wallet_can_spend() already updated both cache and db together, but wallet_get_newindex() read the db directly with db_get_intvar() and wrote it back with db_set_intvar(), bypassing the cache. After newaddr, listaddresses and the rescan window read the stale cache, so freshly issued addresses were invisible until restart. Worse, a later wallet_can_spend() could write the stale, lower cache value back to the db. The fix introduces wallet_max_addr_index() and wallet_set_max_addr_index() helpers and uses them everywhere, ensuring cache and db consistency.
Changed components
wallet/wallet.cwallet/wallet.haddress index cache (bip32_max_index, bip86_max_index)wallet_get_newindex()wallet_can_spend()listaddressesrescan window / keyscan_gap logicInspect captured patch +37 / −29
diff --git a/wallet/wallet.c b/wallet/wallet.c
index 7c54ad1..ff71796 100644
--- a/wallet/wallet.c
+++ b/wallet/wallet.c
@@ -1026,18 +1026,40 @@ bool wallet_add_onchaind_utxo(struct wallet *w,
return true;
}
+/* ADDR_P2TR counts in the bip86 index, everything else in the bip32 one. */
+static u64 wallet_max_addr_index(struct wallet *w, enum addrtype addrtype)
+{
+ if (addrtype == ADDR_P2TR)
+ return w->bip86_max_index;
+ return w->bip32_max_index;
+}
+
+/* The max index is cached in struct wallet, and the db intvar is only
+ * read back at startup, so all updates must go through here. */
+static void wallet_set_max_addr_index(struct wallet *w, enum addrtype addrtype,
+ u64 index)
+{
+ if (addrtype == ADDR_P2TR) {
+ w->bip86_max_index = index;
+ db_set_intvar(w->db, "bip86_max_index", index);
+ } else {
+ w->bip32_max_index = index;
+ db_set_intvar(w->db, "bip32_max_index", index);
+ }
+}
+
bool wallet_can_spend(struct wallet *w, const u8 *script, size_t script_len,
u32 *index, enum addrtype *addrtype)
{
- u64 bip32_max_index, bip86_max_index;
const struct wallet_address *waddr;
struct script_with_len scriptwl = {script, script_len};
-
- bip32_max_index = w->bip32_max_index;
- bip86_max_index = w->bip86_max_index;
+ /* BIP86-based wallets derive all addresses via BIP86, legacy
+ * wallets via BIP32, regardless of the individual addrtype. */
+ enum addrtype index_type = w->ld->bip86_base ? ADDR_P2TR : ADDR_BECH32;
/* Scan both BIP32 and BIP86 addresses */
- u64 max_index = (bip32_max_index > bip86_max_index) ? bip32_max_index : bip86_max_index;
+ u64 max_index = (w->bip32_max_index > w->bip86_max_index)
+ ? w->bip32_max_index : w->bip86_max_index;
while (w->our_addresses_maxindex < max_index + w->keyscan_gap)
our_addresses_add_for_index(w, ++w->our_addresses_maxindex);
@@ -1047,19 +1069,8 @@ bool wallet_can_spend(struct wallet *w, const u8 *script, size_t script_len,
/* If we found a used key in the keyscan_gap we should
* remember that. */
- if (w->ld->bip86_base) {
- /* BIP86-based wallet: all addresses use BIP86 derivation */
- if (waddr->index > bip86_max_index) {
- w->bip86_max_index = waddr->index;
- db_set_intvar(w->db, "bip86_max_index", waddr->index);
- }
- } else {
- /* Legacy wallet: all addresses use BIP32 derivation */
- if (waddr->index > bip32_max_index) {
- db_set_intvar(w->db, "bip32_max_index", waddr->index);
- w->bip32_max_index = waddr->index;
- }
- }
+ if (waddr->index > wallet_max_addr_index(w, index_type))
+ wallet_set_max_addr_index(w, index_type, waddr->index);
*index = waddr->index;
if (addrtype)
@@ -1071,21 +1082,16 @@ s64 wallet_get_newindex(struct lightningd *ld, enum addrtype addrtype)
{
struct db_stmt *stmt;
u64 newidx;
- const char *index_var;
-
- /* Choose index variable based on wallet type */
- if (ld->bip86_base) {
- index_var = "bip86_max_index";
- } else {
- index_var = "bip32_max_index";
- }
+ /* Choose index based on wallet type, not on the addrtype being
+ * issued: there is a single index space per wallet. */
+ enum addrtype index_type = ld->bip86_base ? ADDR_P2TR : ADDR_BECH32;
- newidx = db_get_intvar(ld->wallet->db, index_var, 0) + 1;
+ newidx = wallet_max_addr_index(ld->wallet, index_type) + 1;
if (newidx == BIP32_INITIAL_HARDENED_CHILD)
return -1;
- db_set_intvar(ld->wallet->db, index_var, newidx);
+ wallet_set_max_addr_index(ld->wallet, index_type, newidx);
stmt = db_prepare_v2(ld->wallet->db,
SQL("INSERT INTO addresses ("
" keyidx"
diff --git a/wallet/wallet.h b/wallet/wallet.h
index c110b8b..967804d 100644
--- a/wallet/wallet.h
+++ b/wallet/wallet.h
@@ -57,7 +57,9 @@ struct wallet {
/* How many keys should we look ahead at most? */
u64 keyscan_gap;
- /* Address lookahead max index. */
+ /* Address lookahead max index: in-memory copy of the
+ * bip32_max_index/bip86_max_index db vars, all updates must go
+ * through wallet_set_max_addr_index. */
u64 bip32_max_index;
u64 bip86_max_index;
};
Why this scored 51/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.