Prune after coins flush #36681

pull AlleyBo55 wants to merge 3 commits into bitcoin:master from AlleyBo55:prune-after-coins-flush changing 5 files +85 −16
  1. AlleyBo55 commented at 6:25 AM on October 28, 2026: none

    Crash-safe pruning after chainstate persistence

    This change makes prune selection non-mutating, then delays block-index pruning and file deletion until after the UTXO cache write. That closes the demonstrated process-crash window where ReplayBlocks() needs files that were already deleted. The new order still allows durable prune metadata and deletion to outrun a non-synchronous coins write on machine failure, and it no longer reclaims selected files before the chainstate disk-space preflight. Watch for: confirmed machine-crash recovery failure and confirmed low-disk shutdown regression on the default shared filesystem.

    What this branch solves

    This branch addresses one cause of #29422. The issue describes a pruned node restarting with an unusable chainstate after the process was killed during IBD. The reproduced failure happens when pruning deletes block files before the coins database finishes writing. On restart, the older coins database needs those deleted blocks and cannot recover without a full reindex.

    The branch changes that order so the block files remain available until the coins write returns. The new test proves that this fixes the process-crash/OOM-kill path: unpatched master deletes blk00000.dat and fails during restart, while this branch keeps the file and successfully catches up from disk.

    It does not repair a node that is already broken; #36680 adds that startup detection and recovery message. It also does not yet guarantee safety after a whole-machine crash or power loss, because returning from the coins write does not guarantee that every write is durable on disk.

    High-level view

    For process crashes, selected files remain available through the coins transition. The first block-index write protects newly indexed blocks referenced by the coins tip; the second atomically records pruned metadata before unlink, and startup removes files left after that commit.

    Confirmed: the cross-database durability order is incomplete. Coins batches use LevelDB's non-synchronous mode, while the subsequent block-index prune batch is synchronous, so a machine crash can preserve pruning while losing enough of the coins transition to require the deleted files.

    Confirmed: the delayed unlink changes minimum free-space behavior. CheckDiskSpace(datadir, 192 * dirty_count) now runs while selected block files still occupy the default shared filesystem, so pruning can issue a fatal low-disk error where the previous order first created enough room for the forced coins write.

    Manual and automatic pruning use the same new order. Startup retries automatic pruning, while interrupted manual pruning requires another RPC. During assumeutxo background validation, snapshot and historical pruning operate on disjoint height regions backed by separate blockfile cursors.

    Confirmed coverage gap: the added test reproduces one manual-prune process crash during a partial coins batch. It does not exercise automatic pruning, low disk, machine-level durability, the second block-index commit window, or two live chainstates.

    <details> <summary>Issues (3)</summary>

    1. Unsynced coins transition (confirmed) — The synchronous prune metadata commit and unlink can survive a machine crash while the preceding non-synchronous coins batches are lost. Pass a prune-specific durable mode through every coins batch, then synchronously commit prune metadata and unlink.
    2. Space reclaimed too late (confirmed) — On a shared filesystem, the UTXO disk-space check now runs before selected files are removed and can fatally stop a prune that previously made enough room. Pre-prune only files below every live chainstate's replay floor, or reserve the forced coins-write space before block allocation.
    3. Crash-test scope (confirmed) — The test covers one manual partial-batch process exit and checks only height. Add focused low-disk and automatic-retry cases, compare the recovered UTXO hash, and do not treat this hook as machine-crash durability coverage.

    </details>

    <details> <summary>Details</summary>

    Durable prune metadata can outrun the coins database

    Priority 1, confirmed. The new order treats return from CoinsTip().Flush() or CoinsTip().Sync() as the durability boundary (src/validation.cpp:2798-2803), then synchronously commits the pruned block-index state and deletes the files (src/validation.cpp:2813-2829). Both partial and final UTXO batches call m_db->WriteBatch(batch) without the sync argument (src/txdb.cpp:178-180, src/txdb.cpp:192-197), whose default is fSync = false (src/dbwrapper.h:324). LevelDB states that these writes may be lost when the machine crashes, although they survive a process-only crash (src/leveldb/include/leveldb/options.h:173-182).

    BlockTreeDB::WriteBatchSync() gives the later block-index write a stronger guarantee by calling WriteBatch(batch, true) (src/node/blockstorage.cpp:92-102). If power is lost after that batch and unlink, the block database can durably record the replay blocks as pruned while the coins database returns at an old tip or with DB_HEAD_BLOCKS requiring ReplayBlocks(). The replay positions and files are unavailable, recreating the reindex-required failure this change targets.

    The smallest viable correction is to pass a durable-prune mode through the coins batch write and use synchronous writes for every partial and final batch when fFlushForPrune is true. Commit the prune flag and pruned file/index records in the following synchronous block-index transition, then unlink. The tradeoff is fsync latency during prune-forced flushes rather than every periodic UTXO flush.

    Delayed unlink can prevent pruning on a nearly full shared filesystem

    Priority 2, confirmed. The added block unlinks at src/validation.cpp:2829, after the UTXO preflight at src/validation.cpp:2798. That preflight requires 50 MiB + 192 * CoinsTip().GetDirtyCount() of available space (src/util/fs_helpers.cpp:94-98). Block-file allocation only checks that its own allocation leaves the generic 50 MiB reserve (src/flatfile.cpp:59-70), which does not reserve the larger forced UTXO write.

    On the default shared filesystem, choose free space F such that 50 MiB <= F < 50 MiB + 192 * dirty_count, with selected files large enough to raise F above the upper threshold. The old order passed after unlinking; the new order calls FatalError before deleting a file. Automatic pruning reaches this order through FindFilesToPrune() (src/validation.cpp:2747-2749), and manual pruning reaches it through FindFilesToPruneManual() (src/validation.cpp:2740-2744). Separate block and chainstate filesystems do not benefit from either order.

    The smallest crash-safe correction is to split selected files into early-safe and replay-needed sets. Mark files below the replay floor of every live durable chainstate in the first synchronous block-index batch and unlink them before the UTXO space check. Keep files intersecting any old-to-new replay path until the corresponding durable coins flush, then use the second batch and unlink. If no selected file lies outside every replay path, the node must reserve chainstate write space earlier or fail without pruning.

    The second block-index write closes process-crash windows

    select files
      -> flush block data
      -> sync current block index
      -> write coins transition
      -> mark selected files pruned in memory
      -> sync pruned file/index records
      -> unlink files
    

    A process exit before or during the coins transition leaves block-index positions and files available to ReplayBlocks(). An exit after the coins transition but before the second block-index batch leaves an advanced coins database plus a conservative block index and intact files. The second batch atomically writes zeroed CBlockFileInfo records and cleared BLOCK_HAVE_DATA/BLOCK_HAVE_UNDO entries (src/node/blockstorage.cpp:270-297, src/node/blockstorage.cpp:527-544). If exit occurs after that batch but before or during unlink, startup retries deletion from zero-sized file records (src/node/blockstorage.cpp:608-623). A thrown second database write skips unlink, leaving the previous durable index matched with intact files.

    The second write is necessary and sufficient for process-crash ordering. It cannot act as a durability barrier for the separate coins database addressed in Priority 1.

    Manual, automatic, and assumeutxo pruning remain separated

    Both helpers now only select file numbers (src/node/blockstorage.cpp:315-325, src/node/blockstorage.cpp:357-401) before converging on the post-coins pruning block (src/validation.cpp:2740-2753, src/validation.cpp:2813-2829). Automatic selection clears the request flag, but startup calls PruneAndFlush() for every chainstate (src/init.cpp:2043-2051), which retries target-based pruning after a process crash. The manual target is a sentinel, so an interrupted manual prune waits for another pruneblockchain call.

    For an unvalidated snapshot, GetPruneRange() starts snapshot-chain pruning above the snapshot base (src/validation.cpp:6381-6386); the historical chain prunes below its own lagging tip. BlockfileTypeForHeight() assigns those regions to separate normal and assumed cursors (src/node/blockstorage.cpp:787-793). Each selected range is therefore flushed by the chainstate whose replay can depend on it.

    The crash test covers only the first interrupted coins batch

    Confirmed coverage gap. The test fixes the mode to manual pruning (test/functional/feature_prune_crash.py:22-23) and uses -dbcrashratio=1 to exit after the first partial coins batch (test/functional/feature_prune_crash.py:31-39; hook at src/txdb.cpp:178-187). It checks that blk00000.dat remains, restart reaches the prior in-memory tip, and a repeated manual prune deletes the file (test/functional/feature_prune_crash.py:43-54).

    Because LevelDB's non-synchronous writes survive process exit, this hook cannot test machine-crash durability. There is also no hook after the final coins batch or around the second block-index write, no automatic-prune crash case, and no two-chainstate crash case. Add focused coverage for the low-disk preflight and automatic-prune restart path, and compare recovered UTXO state with a reference hash rather than checking only height. Enforce durable ordering in production code because the functional harness cannot reliably emulate lost kernel writeback state.

    </details>

    <details> <summary>File map</summary>

    • src/node/blockstorage.cpp: makes manual and automatic prune discovery selection-only.
    • src/node/blockstorage.h: documents selection, marking, and unlink responsibilities.
    • src/validation.cpp: moves prune marking, persistence, and unlink after the coins write and adds the second block-index batch.
    • test/functional/feature_prune_crash.py: reproduces a manual-prune process crash during a partial coins batch.
    • test/functional/test_runner.py: registers the new functional test.

    Full diff: git diff upstream/master...HEAD

    </details>

  2. refactor: Select files to prune without marking
    Make FindFilesToPrune() and FindFilesToPruneManual() only select the
    block files to prune. Their only caller, FlushStateToDisk(), now marks
    the selected files as pruned with PruneOneBlockFile().
    
    This does not change behavior, because the files are still marked right
    after they are selected. It allows the next commit to mark them at a
    later point in FlushStateToDisk().
    478fae1419
  3. validation: Prune block files only after flushing the chainstate
    FlushStateToDisk() selects the block files to prune, marks the affected
    blocks as no longer stored in the block index, writes that index to
    disk, deletes the files, and only then flushes the chainstate (the
    coins database).
    
    A prune is selected against the in-memory chain tip, so it can cover
    blocks newer than the best block stored in the on-disk chainstate. If
    the node crashes after the files are deleted but before the chainstate
    flush completes, the on-disk chainstate is still at the previous flush
    and needs those now-deleted blocks to catch up. On the next startup
    ReplayBlocks() cannot read them, and the node can only recover with a
    full reindex, re-downloading the entire chain.
    
    Move the marking, the block index write for the pruned state, and the
    file deletion to after the chainstate flush. Until the flush completes
    the blocks stay on disk, so an interrupted prune leaves a consistent
    state that the node can replay from on the next start. The block index
    is written again after marking so that it never refers to files that
    have already been deleted.
    
    This adds one extra block index write per prune event and keeps the
    pruned files on disk slightly longer, raising the peak disk usage
    during a pruning flush.
    41bbc150fc
  4. test: Add crash recovery test for a pruning flush
    Mine blocks past the last chainstate flush on disk, then start a manual
    prune and crash during the chainstate write using -dbcrashratio. Check
    that on the next start the node catches up from its own block files,
    instead of failing because the blocks it still needs were pruned.
    
    Without the previous commit the pruned files are deleted before the
    crash, so the node cannot replay them and startup fails with "Unable to
    replay blocks".
    911601b3f8
  5. DrahtBot commented at 6:26 AM on October 28, 2026: contributor

    <!--e57a25ab6845829454e8d69fc972939a-->

    The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

    <!--006a51241073e994b41acfe9ec718e94-->

    External sites

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

    See the guideline and AI policy for information on the review process. A summary of reviews will appear here.

    <!--5faf32d7da4f0f540f40219e4f7537a3-->

  6. maflcko commented at 6:35 AM on October 28, 2026: member

    Thx, but LLM output is not accepted if the author can not properly explain, test or could have written the change themselves. Also, LLM generated content is not allowed in comments/communications. See https://github.com/bitcoin/bitcoin/blob/master/doc/AI_POLICY.md

    The bottleneck in this project has always been review and testing, not writing code. Development here is intentionally conservative and slow, and reviewer attention is the scarcest resource we have. LLMs have made this worse, anyone can now prompt them and post their output as PRs. There is an infinite amount plausible looking "improvements" for LLMs to suggest and work on.

    Unless we fully trust LLMs to both write and review code, humans still have to spend time understanding the proposed changes, which incurs a non-zero cost for every opened PR.

    I understand that contributing to this project can be intimidating, and using LLMs may seem tempting, but it really creates more issues for this project than it solves. The best way to help this project, is to review and test changes. You can use LLMs for this, but you shouldn't solely rely on them, or just post their output.

    I am asking you to reconsider whether it's something you genuinely think the project should pursue, independent of what your LLM suggested.

    See https://github.com/bitcoin/bitcoin/blob/master/doc/AI_POLICY.md

  7. maflcko closed this on Oct 28, 2026


github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin/bitcoin. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2026-10-30 15:51 UTC

This site is hosted by @0xB10C
More mirrored repositories can be found on mirror.b10c.me