diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -963,6 +963,7 @@
 		avalanche/proof.cpp
 		avalanche/proofid.cpp
 		avalanche/proofpool.cpp
+		avalanche/stakecontendercache.cpp
 		avalanche/voterecord.cpp
 		cashaddr.cpp         # via cashaddrenc.cpp
 		cashaddrenc.cpp      # via key_io.cpp
diff --git a/src/avalanche/processor.h b/src/avalanche/processor.h
--- a/src/avalanche/processor.h
+++ b/src/avalanche/processor.h
@@ -10,6 +10,7 @@
 #include <avalanche/proof.h>
 #include <avalanche/proofcomparator.h>
 #include <avalanche/protocol.h>
+#include <avalanche/stakecontendercache.h>
 #include <avalanche/voterecord.h> // For AVALANCHE_MAX_INFLIGHT_POLL
 #include <blockindex.h>
 #include <blockindexcomparators.h>
@@ -244,6 +245,9 @@
     std::unordered_map<BlockHash, StakingReward, SaltedUint256Hasher>
         stakingRewards GUARDED_BY(cs_stakingRewards);
 
+    mutable Mutex cs_stakeContenderCache;
+    StakeContenderCache stakeContenderCache GUARDED_BY(cs_stakeContenderCache);
+
     Processor(Config avaconfig, interfaces::Chain &chain, CConnman *connmanIn,
               ChainstateManager &chainman, CTxMemPool *mempoolIn,
               CScheduler &scheduler, std::unique_ptr<PeerData> peerDataIn,
@@ -366,6 +370,12 @@
                       const CNode &node) override LOCKS_EXCLUDED(cs_main)
         EXCLUSIVE_LOCKS_REQUIRED(!cs_peerManager, !cs_delayedAvahelloNodeIds);
 
+    /** Track votes on stake contenders */
+    void addStakeContender(const ProofRef &proof)
+        EXCLUSIVE_LOCKS_REQUIRED(cs_main, !cs_stakeContenderCache);
+    int getStakeContenderStatus(const StakeContenderId &contenderId) const
+        EXCLUSIVE_LOCKS_REQUIRED(!cs_stakeContenderCache);
+
 private:
     void updatedBlockTip()
         EXCLUSIVE_LOCKS_REQUIRED(!cs_peerManager, !cs_finalizedItems);
diff --git a/src/avalanche/processor.cpp b/src/avalanche/processor.cpp
--- a/src/avalanche/processor.cpp
+++ b/src/avalanche/processor.cpp
@@ -970,6 +970,19 @@
     WITH_LOCK(cs_delayedAvahelloNodeIds, delayedAvahelloNodeIds.erase(nodeid));
 }
 
+void Processor::addStakeContender(const ProofRef &proof) {
+    AssertLockHeld(cs_main);
+    const CBlockIndex *activeTip = chainman.ActiveTip();
+    WITH_LOCK(cs_stakeContenderCache,
+              return stakeContenderCache.add(activeTip, proof));
+}
+
+int Processor::getStakeContenderStatus(
+    const StakeContenderId &contenderId) const {
+    return WITH_LOCK(cs_stakeContenderCache,
+                     return stakeContenderCache.getVoteStatus(contenderId));
+}
+
 void Processor::updatedBlockTip() {
     const bool registerLocalProof = canShareLocalProof();
     auto registerProofs = [&]() {
diff --git a/src/avalanche/test/processor_tests.cpp b/src/avalanche/test/processor_tests.cpp
--- a/src/avalanche/test/processor_tests.cpp
+++ b/src/avalanche/test/processor_tests.cpp
@@ -2391,4 +2391,24 @@
     BOOST_CHECK(m_processor->reconcileOrFinalize(betterProof));
 }
 
+BOOST_AUTO_TEST_CASE(stake_contenders) {
+    ChainstateManager &chainman = *Assert(m_node.chainman);
+    Chainstate &active_chainstate = chainman.ActiveChainstate();
+    const CBlockIndex *chaintip =
+        WITH_LOCK(chainman.GetMutex(), return chainman.ActiveTip());
+
+    auto proof = buildRandomProof(active_chainstate, MIN_VALID_PROOF_SCORE);
+    StakeContenderId contenderId(chaintip->GetBlockHash(), proof->getId());
+
+    // Stake contender isn't in the cache yet
+    BOOST_CHECK_EQUAL(m_processor->getStakeContenderStatus(contenderId), -1);
+
+    // Add stake contender to the cache. It defaults to rejected.
+    {
+        LOCK(cs_main);
+        m_processor->addStakeContender(proof);
+    }
+    BOOST_CHECK_EQUAL(m_processor->getStakeContenderStatus(contenderId), 1);
+}
+
 BOOST_AUTO_TEST_SUITE_END()