diff --git a/src/node/blockstorage.h b/src/node/blockstorage.h
--- a/src/node/blockstorage.h
+++ b/src/node/blockstorage.h
@@ -13,6 +13,7 @@
 #include <chain.h>
 #include <chainparams.h>
 #include <kernel/blockmanager_opts.h>
+#include <kernel/chain.h>
 #include <kernel/cs_main.h>
 #include <protocol.h>
 #include <sync.h>
@@ -64,6 +65,34 @@
     int height_first{std::numeric_limits<int>::max()};
 };
 
+enum BlockfileType {
+    // Values used as array indexes - do not change carelessly.
+    NORMAL = 0,
+    ASSUMED = 1,
+    NUM_TYPES = 2,
+};
+
+std::ostream &operator<<(std::ostream &os, const BlockfileType &type);
+
+struct BlockfileCursor {
+    // The latest blockfile number.
+    int file_num{0};
+
+    // Track the height of the highest block in file_num whose undo
+    // data has been written. Block data is written to block files in download
+    // order, but is written to undo files in validation order, which is
+    // usually in order by height. To avoid wasting disk space, undo files will
+    // be trimmed whenever the corresponding block file is finalized and
+    // the height of the highest block written to the block file equals the
+    // height of the highest block written to the undo file. This is a
+    // heuristic and can sometimes preemptively trim undo files that will write
+    // more data later, and sometimes fail to trim undo files that can't have
+    // more data written later.
+    int undo_height{0};
+};
+
+std::ostream &operator<<(std::ostream &os, const BlockfileCursor &cursor);
+
 /**
  * Maintains a tree of blocks (stored in `m_block_index`) which is consulted
  * to determine where the most-work tip is.
@@ -89,8 +118,8 @@
         EXCLUSIVE_LOCKS_REQUIRED(cs_main);
 
     /** Return false if block file or undo file flushing fails. */
-    [[nodiscard]] bool FlushBlockFile(bool fFinalize = false,
-                                      bool finalize_undo = false);
+    [[nodiscard]] bool FlushBlockFile(int blockfile_num, bool fFinalize,
+                                      bool finalize_undo);
 
     /** Return false if undo file flushing fails. */
     [[nodiscard]] bool FlushUndoFile(int block_file, bool finalize = false);
@@ -98,6 +127,7 @@
     [[nodiscard]] bool FindBlockPos(FlatFilePos &pos, unsigned int nAddSize,
                                     unsigned int nHeight, uint64_t nTime,
                                     bool fKnown);
+    [[nodiscard]] bool FlushChainstateBlockFile(int tip_height);
     bool FindUndoPos(BlockValidationState &state, int nFile, FlatFilePos &pos,
                      unsigned int nAddSize);
 
@@ -150,19 +180,31 @@
 
     RecursiveMutex cs_LastBlockFile;
     std::vector<CBlockFileInfo> m_blockfile_info;
-    int m_last_blockfile = 0;
 
-    // Track the height of the highest block in m_last_blockfile whose undo
-    // data has been written. Block data is written to block files in download
-    // order, but is written to undo files in validation order, which is
-    // usually in order by height. To avoid wasting disk space, undo files will
-    // be trimmed whenever the corresponding block file is finalized and
-    // the height of the highest block written to the block file equals the
-    // height of the highest block written to the undo file. This is a
-    // heuristic and can sometimes preemptively trim undo files that will write
-    // more data later, and sometimes fail to trim undo files that can't have
-    // more data written later.
-    unsigned int m_undo_height_in_last_blockfile = 0;
+    //! Since assumedvalid chainstates may be syncing a range of the chain that
+    //! is very far away from the normal/background validation process, we
+    //! should segment blockfiles for assumed chainstates. Otherwise, we might
+    //! have wildly different height ranges mixed into the same block files,
+    //! which would impair our ability to prune effectively.
+    //!
+    //! This data structure maintains separate blockfile number cursors for each
+    //! BlockfileType. The ASSUMED state is initialized, when necessary, in
+    //! FindBlockPos().
+    //!
+    //! The first element is the NORMAL cursor, second is ASSUMED.
+    std::array<std::optional<BlockfileCursor>, BlockfileType::NUM_TYPES>
+        m_blockfile_cursors GUARDED_BY(cs_LastBlockFile) = {{
+            BlockfileCursor{},
+            std::nullopt,
+        }};
+    int MaxBlockfileNum() const EXCLUSIVE_LOCKS_REQUIRED(cs_LastBlockFile) {
+        static const BlockfileCursor empty_cursor;
+        const auto &normal =
+            m_blockfile_cursors[BlockfileType::NORMAL].value_or(empty_cursor);
+        const auto &assumed =
+            m_blockfile_cursors[BlockfileType::ASSUMED].value_or(empty_cursor);
+        return std::max(normal.file_num, assumed.file_num);
+    }
 
     /**
      * Global flag to indicate we should check to see if there are
@@ -189,6 +231,8 @@
     std::unordered_map<std::string, PruneLockInfo>
         m_prune_locks GUARDED_BY(::cs_main);
 
+    BlockfileType BlockfileTypeForHeight(int height);
+
     const kernel::BlockManagerOpts m_opts;
 
 public:
@@ -201,6 +245,20 @@
 
     BlockMap m_block_index GUARDED_BY(cs_main);
 
+    /**
+     * The height of the base block of an assumeutxo snapshot, if one is in use.
+     *
+     * This controls how blockfiles are segmented by chainstate type to avoid
+     * comingling different height regions of the chain when an assumedvalid
+     * chainstate is in use. If heights are drastically different in the same
+     * blockfile, pruning suffers.
+     *
+     * This is set during ActivateSnapshot() or upon LoadBlockIndex() if a
+     * snapshot had been previously loaded. After the snapshot is validated,
+     * this is unset to restore normal LoadBlockIndex behavior.
+     */
+    std::optional<int> m_snapshot_height;
+
     std::vector<CBlockIndex *> GetAllBlockIndices()
         EXCLUSIVE_LOCKS_REQUIRED(::cs_main);
 
diff --git a/src/node/blockstorage.cpp b/src/node/blockstorage.cpp
--- a/src/node/blockstorage.cpp
+++ b/src/node/blockstorage.cpp
@@ -13,6 +13,7 @@
 #include <consensus/validation.h>
 #include <flatfile.h>
 #include <hash.h>
+#include <kernel/chain.h>
 #include <kernel/chainparams.h>
 #include <logging.h>
 #include <pow/pow.h>
@@ -142,7 +143,8 @@
     const auto [min_block_to_prune, last_block_can_prune] =
         chainman.GetPruneRange(chain, nManualPruneHeight);
     int count = 0;
-    for (int fileNumber = 0; fileNumber < m_last_blockfile; fileNumber++) {
+    for (int fileNumber = 0; fileNumber < this->MaxBlockfileNum();
+         fileNumber++) {
         const auto &fileinfo = m_blockfile_info[fileNumber];
         if (fileinfo.nSize == 0 ||
             fileinfo.nHeightLast > (unsigned)last_block_can_prune ||
@@ -196,7 +198,8 @@
             nBuffer += target / 10;
         }
 
-        for (int fileNumber = 0; fileNumber < m_last_blockfile; fileNumber++) {
+        for (int fileNumber = 0; fileNumber < this->MaxBlockfileNum();
+             fileNumber++) {
             const auto &fileinfo = m_blockfile_info[fileNumber];
             nBytesToPrune = fileinfo.nSize + fileinfo.nUndoSize;
 
@@ -263,11 +266,10 @@
         return false;
     }
 
-    int snapshot_height = -1;
     if (snapshot_blockhash) {
         const AssumeutxoData au_data =
             *Assert(GetParams().AssumeutxoForBlockhash(*snapshot_blockhash));
-        snapshot_height = au_data.height;
+        m_snapshot_height = au_data.height;
         CBlockIndex *base{LookupBlockIndex(*snapshot_blockhash)};
 
         // Since nChainTx (responsible for estimated progress) isn't persisted
@@ -276,8 +278,16 @@
         base->nChainTx = au_data.nChainTx;
         LogPrintf("[snapshot] set nChainTx=%d for %s\n", au_data.nChainTx,
                   snapshot_blockhash->ToString());
+    } else {
+        // If this isn't called with a snapshot blockhash, make sure the cached
+        // snapshot height is null. This is relevant during snapshot
+        // completion, when the blockman may be loaded with a height that then
+        // needs to be cleared after the snapshot is fully validated.
+        m_snapshot_height.reset();
     }
 
+    Assert(m_snapshot_height.has_value() == snapshot_blockhash.has_value());
+
     // Calculate nChainWork
     std::vector<CBlockIndex *> vSortedByHeight{GetAllBlockIndices()};
     std::sort(vSortedByHeight.begin(), vSortedByHeight.end(),
@@ -298,7 +308,7 @@
         // basis of snapshot load (see PopulateAndValidateSnapshot()).
         // Pruned nodes may have deleted the block.
         if (pindex->nTx > 0) {
-            if (snapshot_blockhash && pindex->nHeight == snapshot_height &&
+            if (m_snapshot_height && pindex->nHeight == *m_snapshot_height &&
                 pindex->GetBlockHash() == *snapshot_blockhash) {
                 Assert(pindex->pprev);
                 // Should have been set above; don't disturb it with code below.
@@ -340,7 +350,9 @@
 
     m_dirty_blockindex.clear();
 
-    if (!m_block_tree_db->WriteBatchSync(vFiles, m_last_blockfile, vBlocks)) {
+    int max_blockfile =
+        WITH_LOCK(cs_LastBlockFile, return this->MaxBlockfileNum());
+    if (!m_block_tree_db->WriteBatchSync(vFiles, max_blockfile, vBlocks)) {
         return false;
     }
     return true;
@@ -351,17 +363,18 @@
     if (!LoadBlockIndex(snapshot_blockhash)) {
         return false;
     }
+    int max_blockfile_num{0};
 
     // Load block file info
-    m_block_tree_db->ReadLastBlockFile(m_last_blockfile);
-    m_blockfile_info.resize(m_last_blockfile + 1);
-    LogPrintf("%s: last block file = %i\n", __func__, m_last_blockfile);
-    for (int nFile = 0; nFile <= m_last_blockfile; nFile++) {
+    m_block_tree_db->ReadLastBlockFile(max_blockfile_num);
+    m_blockfile_info.resize(max_blockfile_num + 1);
+    LogPrintf("%s: last block file = %i\n", __func__, max_blockfile_num);
+    for (int nFile = 0; nFile <= max_blockfile_num; nFile++) {
         m_block_tree_db->ReadBlockFileInfo(nFile, m_blockfile_info[nFile]);
     }
     LogPrintf("%s: last block file info: %s\n", __func__,
-              m_blockfile_info[m_last_blockfile].ToString());
-    for (int nFile = m_last_blockfile + 1; true; nFile++) {
+              m_blockfile_info[max_blockfile_num].ToString());
+    for (int nFile = max_blockfile_num + 1; true; nFile++) {
         CBlockFileInfo info;
         if (m_block_tree_db->ReadBlockFileInfo(nFile, info)) {
             m_blockfile_info.push_back(info);
@@ -387,6 +400,16 @@
         }
     }
 
+    {
+        // Initialize the blockfile cursors.
+        LOCK(cs_LastBlockFile);
+        for (size_t i = 0; i < m_blockfile_info.size(); ++i) {
+            const auto last_height_in_file = m_blockfile_info[i].nHeightLast;
+            m_blockfile_cursors[BlockfileTypeForHeight(last_height_in_file)] = {
+                static_cast<int>(i), 0};
+        }
+    }
+
     // Check whether we have ever pruned block & undo files
     m_block_tree_db->ReadFlag("prunedblockfiles", m_have_pruned);
     if (m_have_pruned) {
@@ -404,12 +427,14 @@
 
 void BlockManager::ScanAndUnlinkAlreadyPrunedFiles() {
     AssertLockHeld(::cs_main);
+    int max_blockfile =
+        WITH_LOCK(cs_LastBlockFile, return this->MaxBlockfileNum());
     if (!m_have_pruned) {
         return;
     }
 
     std::set<int> block_files_to_prune;
-    for (int file_number = 0; file_number < m_last_blockfile; file_number++) {
+    for (int file_number = 0; file_number < max_blockfile; file_number++) {
         if (m_blockfile_info[file_number].nSize == 0) {
             block_files_to_prune.insert(file_number);
         }
@@ -592,7 +617,8 @@
     return true;
 }
 
-bool BlockManager::FlushBlockFile(bool fFinalize, bool finalize_undo) {
+bool BlockManager::FlushBlockFile(int blockfile_num, bool fFinalize,
+                                  bool finalize_undo) {
     bool success = true;
     LOCK(cs_LastBlockFile);
 
@@ -604,10 +630,10 @@
         // have populated `m_blockfile_info` via LoadBlockIndexDB().
         return true;
     }
-    assert(static_cast<int>(m_blockfile_info.size()) > m_last_blockfile);
+    assert(static_cast<int>(m_blockfile_info.size()) > blockfile_num);
 
-    FlatFilePos block_pos_old(m_last_blockfile,
-                              m_blockfile_info[m_last_blockfile].nSize);
+    FlatFilePos block_pos_old(blockfile_num,
+                              m_blockfile_info[blockfile_num].nSize);
     if (!BlockFileSeq().Flush(block_pos_old, fFinalize)) {
         AbortNode("Flushing block file to disk failed. This is likely the "
                   "result of an I/O error.");
@@ -617,13 +643,35 @@
     // behind the incoming blocks,
     // e.g. during IBD or a sync after a node going offline
     if (!fFinalize || finalize_undo) {
-        if (!FlushUndoFile(m_last_blockfile, finalize_undo)) {
+        if (!FlushUndoFile(blockfile_num, finalize_undo)) {
             success = false;
         }
     }
     return success;
 }
 
+BlockfileType BlockManager::BlockfileTypeForHeight(int height) {
+    if (!m_snapshot_height) {
+        return BlockfileType::NORMAL;
+    }
+    return (height >= *m_snapshot_height) ? BlockfileType::ASSUMED
+                                          : BlockfileType::NORMAL;
+}
+
+bool BlockManager::FlushChainstateBlockFile(int tip_height) {
+    LOCK(cs_LastBlockFile);
+    auto &cursor = m_blockfile_cursors[BlockfileTypeForHeight(tip_height)];
+    // If the cursor does not exist, it means an assumeutxo snapshot is loaded,
+    // but no blocks past the snapshot height have been written yet, so there
+    // is no data associated with the chainstate, and it is safe not to flush.
+    if (cursor) {
+        return FlushBlockFile(cursor->file_num, /*fFinalize=*/false,
+                              /*finalize_undo=*/false);
+    }
+    // No need to log warnings in this case.
+    return true;
+}
+
 uint64_t BlockManager::CalculateCurrentUsage() {
     LOCK(cs_LastBlockFile);
 
@@ -680,8 +728,22 @@
                                 bool fKnown) {
     LOCK(cs_LastBlockFile);
 
-    unsigned int nFile = fKnown ? pos.nFile : m_last_blockfile;
-    if (m_blockfile_info.size() <= nFile) {
+    const BlockfileType chain_type = BlockfileTypeForHeight(nHeight);
+
+    if (!m_blockfile_cursors[chain_type]) {
+        // If a snapshot is loaded during runtime, we may not have initialized
+        // this cursor yet.
+        assert(chain_type == BlockfileType::ASSUMED);
+        const auto new_cursor = BlockfileCursor{this->MaxBlockfileNum() + 1};
+        m_blockfile_cursors[chain_type] = new_cursor;
+        LogPrint(BCLog::BLOCKSTORE,
+                 "[%s] initializing blockfile cursor to %s\n", chain_type,
+                 new_cursor);
+    }
+    const int last_blockfile = m_blockfile_cursors[chain_type]->file_num;
+
+    int nFile = fKnown ? pos.nFile : last_blockfile;
+    if (static_cast<int>(m_blockfile_info.size()) <= nFile) {
         m_blockfile_info.resize(nFile + 1);
     }
 
@@ -702,15 +764,22 @@
         //   or raise MAX_BLOCKFILE_SIZE when we reach block sizes larger than
         //   128 MiB
         assert(nAddSize < max_blockfile_size);
+
         while (m_blockfile_info[nFile].nSize + nAddSize >= max_blockfile_size) {
             // when the undo file is keeping up with the block file, we want to
             // flush it explicitly when it is lagging behind (more blocks arrive
             // than are being connected), we let the undo block write case
             // handle it
-            finalize_undo = (m_blockfile_info[nFile].nHeightLast ==
-                             m_undo_height_in_last_blockfile);
-            nFile++;
-            if (m_blockfile_info.size() <= nFile) {
+            finalize_undo =
+                (static_cast<int>(m_blockfile_info[nFile].nHeightLast) ==
+                 Assert(m_blockfile_cursors[chain_type])->undo_height);
+
+            // Try the next unclaimed blockfile number
+            nFile = this->MaxBlockfileNum() + 1;
+            // Set to increment MaxBlockfileNum() for next iteration
+            m_blockfile_cursors[chain_type] = BlockfileCursor{nFile};
+
+            if (static_cast<int>(m_blockfile_info.size()) <= nFile) {
                 m_blockfile_info.resize(nFile + 1);
             }
         }
@@ -718,11 +787,13 @@
         pos.nPos = m_blockfile_info[nFile].nSize;
     }
 
-    if ((int)nFile != m_last_blockfile) {
+    if (nFile != last_blockfile) {
         if (!fKnown) {
-            LogPrint(BCLog::BLOCKSTORE, "Leaving block file %i: %s\n",
-                     m_last_blockfile,
-                     m_blockfile_info[m_last_blockfile].ToString());
+            LogPrint(BCLog::BLOCKSTORE,
+                     "Leaving block file %i: %s (onto %i) (height %i)\n",
+                     last_blockfile,
+                     m_blockfile_info[last_blockfile].ToString(), nFile,
+                     nHeight);
         }
 
         // Do not propagate the return code. The flush concerns a previous block
@@ -732,16 +803,15 @@
         // data may be inconsistent after a crash if the flush is called during
         // a reindex. A flush error might also leave some of the data files
         // untrimmed.
-        if (!FlushBlockFile(!fKnown, finalize_undo)) {
+        if (!FlushBlockFile(last_blockfile, !fKnown, finalize_undo)) {
             LogPrintLevel(
                 BCLog::BLOCKSTORE, BCLog::Level::Warning,
                 "Failed to flush previous block file %05i (finalize=%i, "
                 "finalize_undo=%i) before opening new block file %05i\n",
-                m_last_blockfile, !fKnown, finalize_undo, nFile);
+                last_blockfile, !fKnown, finalize_undo, nFile);
         }
-        m_last_blockfile = nFile;
         // No undo data yet in the new file, so reset our undo-height tracking.
-        m_undo_height_in_last_blockfile = 0;
+        m_blockfile_cursors[chain_type] = BlockfileCursor{nFile};
     }
 
     m_blockfile_info[nFile].AddBlock(nHeight, nTime);
@@ -822,6 +892,10 @@
                                          BlockValidationState &state,
                                          CBlockIndex &block) {
     AssertLockHeld(::cs_main);
+    const BlockfileType type = BlockfileTypeForHeight(block.nHeight);
+    auto &cursor =
+        *Assert(WITH_LOCK(cs_LastBlockFile, return m_blockfile_cursors[type]));
+
     // Write undo information to disk
     if (block.GetUndoPos().IsNull()) {
         FlatFilePos _pos;
@@ -840,7 +914,7 @@
         // does not catch the case where the undo writes are keeping up with the
         // block writes (usually when a synced up node is getting newly mined
         // blocks) -- this case is caught in the FindBlockPos function
-        if (_pos.nFile < m_last_blockfile &&
+        if (_pos.nFile < cursor.file_num &&
             static_cast<uint32_t>(block.nHeight) ==
                 m_blockfile_info[_pos.nFile].nHeightLast) {
             // Do not propagate the return code, a failed flush here should not
@@ -852,10 +926,9 @@
                 LogPrintLevel(BCLog::BLOCKSTORE, BCLog::Level::Warning,
                               "Failed to flush undo file %05i\n", _pos.nFile);
             }
-        } else if (_pos.nFile == m_last_blockfile &&
-                   static_cast<uint32_t>(block.nHeight) >
-                       m_undo_height_in_last_blockfile) {
-            m_undo_height_in_last_blockfile = block.nHeight;
+        } else if (_pos.nFile == cursor.file_num &&
+                   block.nHeight > cursor.undo_height) {
+            cursor.undo_height = block.nHeight;
         }
         // update nUndoPos in block index
         block.nUndoPos = _pos.nPos;
@@ -1110,4 +1183,24 @@
         }
     } // End scope of ImportingNow
 }
+
+std::ostream &operator<<(std::ostream &os, const BlockfileType &type) {
+    switch (type) {
+        case BlockfileType::NORMAL:
+            os << "normal";
+            break;
+        case BlockfileType::ASSUMED:
+            os << "assumed";
+            break;
+        default:
+            os.setstate(std::ios_base::failbit);
+    }
+    return os;
+}
+
+std::ostream &operator<<(std::ostream &os, const BlockfileCursor &cursor) {
+    os << strprintf("BlockfileCursor(file_num=%d, undo_height=%d)",
+                    cursor.file_num, cursor.undo_height);
+    return os;
+}
 } // namespace node
diff --git a/src/validation.cpp b/src/validation.cpp
--- a/src/validation.cpp
+++ b/src/validation.cpp
@@ -2651,7 +2651,8 @@
                     // disk.
                     // TODO: Handle return error, or add detailed comment why
                     // it is safe to not return an error upon failure.
-                    if (!m_blockman.FlushBlockFile()) {
+                    if (!m_blockman.FlushChainstateBlockFile(
+                            m_chain.Height())) {
                         LogPrintLevel(BCLog::VALIDATION, BCLog::Level::Warning,
                                       "%s: Failed to flush block file.\n",
                                       __func__);
@@ -6556,6 +6557,7 @@
         assert(chaintip_loaded);
 
         m_active_chainstate = m_snapshot_chainstate.get();
+        m_blockman.m_snapshot_height = this->GetSnapshotBaseHeight();
 
         LogPrintf("[snapshot] successfully activated snapshot %s\n",
                   base_blockhash.ToString());