diff --git a/src/index/txindex.h b/src/index/txindex.h
--- a/src/index/txindex.h
+++ b/src/index/txindex.h
@@ -15,38 +15,30 @@
 class CBlockIndex;
 
 /**
- * TxIndex is used to look up transactions included in the blockchain by hash.
- * The index is written to a LevelDB database and records the filesystem
- * location of each transaction by transaction hash.
+ * Base class for indices of blockchain data. This implements
+ * CValidationInterface and ensures blocks are indexed sequentially according
+ * to their position in the active chain.
  */
-class TxIndex final : public CValidationInterface {
+class BaseIndex : public CValidationInterface {
 private:
-    const std::unique_ptr<TxIndexDB> m_db;
-
     /// Whether the index is in sync with the main chain. The flag is flipped
     /// from false to true once, after which point this starts processing
     /// ValidationInterface notifications to stay in sync.
-    std::atomic<bool> m_synced;
+    std::atomic<bool> m_synced{false};
 
-    /// The last block in the chain that the TxIndex is in sync with.
-    std::atomic<const CBlockIndex *> m_best_block_index;
+    /// The last block in the chain that the index is in sync with.
+    std::atomic<const CBlockIndex *> m_best_block_index{nullptr};
 
     std::thread m_thread_sync;
     CThreadInterrupt m_interrupt;
 
-    /// Initialize internal state from the database and block index.
-    bool Init();
-
-    /// Sync the tx index with the block index starting from the current best
+    /// Sync the index with the block index starting from the current best
     /// block. Intended to be run in its own thread, m_thread_sync, and can be
-    /// interrupted with m_interrupt. Once the txindex gets in sync, the
-    /// m_synced flag is set and the BlockConnected ValidationInterface callback
-    /// takes over and the sync thread exits.
+    /// interrupted with m_interrupt. Once the index gets in sync, the m_synced
+    /// flag is set and the BlockConnected ValidationInterface callback takes
+    /// over and the sync thread exits.
     void ThreadSync();
 
-    /// Write update index entries for a newly connected block.
-    bool WriteBlock(const CBlock &block, const CBlockIndex *pindex);
-
     /// Write the current chain block locator to the DB.
     bool WriteBestBlock(const CBlockIndex *block_index);
 
@@ -58,20 +50,58 @@
 
     void ChainStateFlushed(const CBlockLocator &locator) override;
 
-public:
-    /// Constructs the TxIndex, which becomes available to be queried.
-    explicit TxIndex(std::unique_ptr<TxIndexDB> db);
+    /// Initialize internal state from the database and block index.
+    virtual bool Init();
 
+    /// Write update index entries for a newly connected block.
+    virtual bool WriteBlock(const CBlock &block, const CBlockIndex *pindex) {
+        return true;
+    }
+
+    virtual BaseIndexDB &GetDB() const = 0;
+
+public:
     /// Destructor interrupts sync thread if running and blocks until it exits.
-    ~TxIndex();
+    virtual ~BaseIndex();
 
-    /// Blocks the current thread until the transaction index is caught up to
-    /// the current state of the block chain. This only blocks if the index has
-    /// gotten in sync once and only needs to process blocks in the
-    /// ValidationInterface queue. If the index is catching up from far behind,
-    /// this method does not block and immediately returns false.
+    /// Blocks the current thread until the index is caught up to the current
+    /// state of the block chain. This only blocks if the index has gotten in
+    /// sync once and only needs to process blocks in the ValidationInterface
+    /// queue. If the index is catching up from far behind, this method does
+    /// not block and immediately returns false.
     bool BlockUntilSyncedToCurrentChain();
 
+    void Interrupt();
+
+    /// Start initializes the sync state and registers the instance as a
+    /// ValidationInterface so that it stays in sync with blockchain updates.
+    void Start();
+
+    /// Stops the instance from staying in sync with blockchain updates.
+    void Stop();
+};
+
+/**
+ * TxIndex is used to look up transactions included in the blockchain by hash.
+ * The index is written to a LevelDB database and records the filesystem
+ * location of each transaction by transaction hash.
+ */
+class TxIndex final : public BaseIndex {
+private:
+    const std::unique_ptr<TxIndexDB> m_db;
+
+protected:
+    /// Override base class init to migrate from old database.
+    bool Init() override;
+
+    bool WriteBlock(const CBlock &block, const CBlockIndex *pindex) override;
+
+    BaseIndexDB &GetDB() const override;
+
+public:
+    /// Constructs the index, which becomes available to be queried.
+    explicit TxIndex(std::unique_ptr<TxIndexDB> db);
+
     /// Look up a transaction by hash.
     ///
     /// @param[in]   tx_hash  The hash of the transaction to be returned.
@@ -81,15 +111,6 @@
     /// @return  true if transaction is found, false otherwise
     bool FindTx(const uint256 &tx_hash, uint256 &block_hash,
                 CTransactionRef &tx) const;
-
-    void Interrupt();
-
-    /// Start initializes the sync state and registers the instance as a
-    /// ValidationInterface so that it stays in sync with blockchain updates.
-    void Start();
-
-    /// Stops the instance from staying in sync with blockchain updates.
-    void Stop();
 };
 
 /// The global transaction index, used in GetTransaction. May be null.
diff --git a/src/index/txindex.cpp b/src/index/txindex.cpp
--- a/src/index/txindex.cpp
+++ b/src/index/txindex.cpp
@@ -27,10 +27,9 @@
     StartShutdown();
 }
 
-TxIndex::TxIndex(std::unique_ptr<TxIndexDB> db)
-    : m_db(std::move(db)), m_synced(false), m_best_block_index(nullptr) {}
+TxIndex::TxIndex(std::unique_ptr<TxIndexDB> db) : m_db(std::move(db)) {}
 
-TxIndex::~TxIndex() {
+BaseIndex::~BaseIndex() {
     Interrupt();
     Stop();
 }
@@ -45,11 +44,16 @@
         return false;
     }
 
+    return BaseIndex::Init();
+}
+
+bool BaseIndex::Init() {
     CBlockLocator locator;
-    if (!m_db->ReadBestBlock(locator)) {
+    if (!GetDB().ReadBestBlock(locator)) {
         locator.SetNull();
     }
 
+    LOCK(cs_main);
     m_best_block_index = FindForkInGlobalIndex(chainActive, locator);
     m_synced = m_best_block_index.load() == chainActive.Tip();
     return true;
@@ -70,7 +74,7 @@
     return chainActive.Next(chainActive.FindFork(pindex_prev));
 }
 
-void TxIndex::ThreadSync() {
+void BaseIndex::ThreadSync() {
     const CBlockIndex *pindex = m_best_block_index.load();
     if (!m_synced) {
         auto &config = GetConfig();
@@ -141,15 +145,19 @@
     return m_db->WriteTxs(vPos);
 }
 
-bool TxIndex::WriteBestBlock(const CBlockIndex *block_index) {
+BaseIndexDB &TxIndex::GetDB() const {
+    return *m_db;
+}
+
+bool BaseIndex::WriteBestBlock(const CBlockIndex *block_index) {
     LOCK(cs_main);
-    if (!m_db->WriteBestBlock(chainActive.GetLocator(block_index))) {
+    if (!GetDB().WriteBestBlock(chainActive.GetLocator(block_index))) {
         return error("%s: Failed to write locator to disk", __func__);
     }
     return true;
 }
 
-void TxIndex::BlockConnected(
+void BaseIndex::BlockConnected(
     const std::shared_ptr<const CBlock> &block, const CBlockIndex *pindex,
     const std::vector<CTransactionRef> &txn_conflicted) {
     if (!m_synced) {
@@ -191,7 +199,7 @@
     }
 }
 
-void TxIndex::ChainStateFlushed(const CBlockLocator &locator) {
+void BaseIndex::ChainStateFlushed(const CBlockLocator &locator) {
     if (!m_synced) {
         return;
     }
@@ -225,12 +233,12 @@
         return;
     }
 
-    if (!m_db->WriteBestBlock(locator)) {
+    if (!GetDB().WriteBestBlock(locator)) {
         error("%s: Failed to write locator to disk", __func__);
     }
 }
 
-bool TxIndex::BlockUntilSyncedToCurrentChain() {
+bool BaseIndex::BlockUntilSyncedToCurrentChain() {
     AssertLockNotHeld(cs_main);
 
     if (!m_synced) {
@@ -279,11 +287,11 @@
     return true;
 }
 
-void TxIndex::Interrupt() {
+void BaseIndex::Interrupt() {
     m_interrupt();
 }
 
-void TxIndex::Start() {
+void BaseIndex::Start() {
     // Need to register this ValidationInterface before running Init(), so that
     // callbacks are not missed if Init sets m_synced to true.
     RegisterValidationInterface(this);
@@ -293,10 +301,10 @@
     }
 
     m_thread_sync = std::thread(&TraceThread<std::function<void()>>, "txindex",
-                                std::bind(&TxIndex::ThreadSync, this));
+                                std::bind(&BaseIndex::ThreadSync, this));
 }
 
-void TxIndex::Stop() {
+void BaseIndex::Stop() {
     UnregisterValidationInterface(this);
 
     if (m_thread_sync.joinable()) {