fix: harden ThreadNotifyWallets IBD read-retry (crash + abandoned disconnects)

Two defects in the IBD ReadBlockFromDisk retry path of ThreadNotifyWallets:

- The connect-loop rebuild indexed recentlyConflicted.first.at(pindex), which
  throws std::out_of_range for a block whose conflict entry was drained in an
  earlier retry cycle. Uncaught under cs_main in the notify boost thread, that
  aborts the node and crash-loops. Use operator[] (empty-list default), i.e.
  best-effort conflict notifications, instead of throwing.

- The disconnect-loop IBD break fell through into the connect loop, which
  advanced pindexLastTip to the new tip and permanently abandoned the pending
  disconnect notifications (leaving wallet witness/anchor state desynced from
  the chain). Add a flag so the break skips the connect loop this cycle and
  truly retries the disconnect on the next one.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-08 05:48:39 +02:00
parent 7914fca0f2
commit 3bb4eb3a5a

View File

@@ -156,10 +156,17 @@ void ThreadNotifyWallets(CBlockIndex *pindexLastTip)
assert(pcoinsTip->GetSaplingAnchorAt(SaplingMerkleTree::empty_root(), oldSaplingTree)); assert(pcoinsTip->GetSaplingAnchorAt(SaplingMerkleTree::empty_root(), oldSaplingTree));
} }
// Use operator[] (empty-list default), NOT .at(): a block's conflict entry can be
// absent if it was drained in an earlier cycle whose connect loop hit the IBD
// ReadBlockFromDisk retry below (that break leaves pindexLastTip behind, so the block
// gets rebuilt here but its conflicts were already cleared and are never re-inserted).
// .at() would throw std::out_of_range which, uncaught under cs_main in this boost
// thread, aborts the node (and crash-loops since pindexLastTip cannot advance). A
// missing entry simply means no conflict notifications for this block (best-effort).
blockStack.emplace_back( blockStack.emplace_back(
pindex, pindex,
std::make_pair(oldSproutTree, oldSaplingTree), std::make_pair(oldSproutTree, oldSaplingTree),
recentlyConflicted.first.at(pindex)); recentlyConflicted.first[pindex]);
pindex = pindex->pprev; pindex = pindex->pprev;
} }
@@ -174,7 +181,11 @@ void ThreadNotifyWallets(CBlockIndex *pindexLastTip)
// network message processing thread. // network message processing thread.
// //
// Notify block disconnects // Notify block disconnects. If an IBD read fails mid-disconnect we must NOT fall through
// to the connect loop (which advances pindexLastTip to the new tip and permanently abandons
// the remaining disconnects -> wallet witness/anchor desync); skip connects this cycle and
// retry the whole disconnect next cycle, exactly as the connect-side break retries.
bool fDisconnectIncomplete = false;
while (pindexLastTip && pindexLastTip != pindexFork) { while (pindexLastTip && pindexLastTip != pindexFork) {
// Read block from disk. // Read block from disk.
CBlock block; CBlock block;
@@ -184,6 +195,7 @@ void ThreadNotifyWallets(CBlockIndex *pindexLastTip)
// Sleep briefly and retry on the next cycle instead of crashing. // Sleep briefly and retry on the next cycle instead of crashing.
LogPrintf("%s: block at height %d not yet readable, will retry\n", LogPrintf("%s: block at height %d not yet readable, will retry\n",
__func__, pindexLastTip->GetHeight()); __func__, pindexLastTip->GetHeight());
fDisconnectIncomplete = true;
break; break;
} }
LogPrintf("*** %s\n", "Failed to read block while notifying wallets of block disconnects"); LogPrintf("*** %s\n", "Failed to read block while notifying wallets of block disconnects");
@@ -205,8 +217,9 @@ void ThreadNotifyWallets(CBlockIndex *pindexLastTip)
pindexLastTip = pindexLastTip->pprev; pindexLastTip = pindexLastTip->pprev;
} }
// Notify block connections // Notify block connections (skipped this cycle if the disconnect loop broke to retry an
while (!blockStack.empty()) { // unreadable block, so pindexLastTip is not advanced past the un-disconnected blocks).
while (!fDisconnectIncomplete && !blockStack.empty()) {
auto blockData = blockStack.back(); auto blockData = blockStack.back();
blockStack.pop_back(); blockStack.pop_back();