diff --git a/src/avalanche/processor.cpp b/src/avalanche/processor.cpp
index 04eaf21df..fd5fc9de2 100644
--- a/src/avalanche/processor.cpp
+++ b/src/avalanche/processor.cpp
@@ -1,1368 +1,1396 @@
 // Copyright (c) 2018-2019 The Bitcoin developers
 // Distributed under the MIT software license, see the accompanying
 // file COPYING or http://www.opensource.org/licenses/mit-license.php.
 
 #include <avalanche/processor.h>
 
 #include <avalanche/avalanche.h>
 #include <avalanche/delegationbuilder.h>
 #include <avalanche/peermanager.h>
 #include <avalanche/proofcomparator.h>
 #include <avalanche/validation.h>
 #include <avalanche/voterecord.h>
 #include <chain.h>
 #include <common/args.h>
 #include <key_io.h> // For DecodeSecret
 #include <net.h>
 #include <netbase.h>
 #include <netmessagemaker.h>
 #include <policy/block/stakingrewards.h>
 #include <scheduler.h>
 #include <util/bitmanip.h>
 #include <util/moneystr.h>
 #include <util/time.h>
 #include <util/translation.h>
 #include <validation.h>
 
 #include <chrono>
 #include <limits>
 #include <tuple>
 
 /**
  * Run the avalanche event loop every 10ms.
  */
 static constexpr std::chrono::milliseconds AVALANCHE_TIME_STEP{10};
 
 static const std::string AVAPEERS_FILE_NAME{"avapeers.dat"};
 
 namespace avalanche {
 static const uint256 GetVoteItemId(const AnyVoteItem &item) {
     return std::visit(variant::overloaded{
                           [](const ProofRef &proof) {
                               uint256 id = proof->getId();
                               return id;
                           },
                           [](const CBlockIndex *pindex) {
                               uint256 hash = pindex->GetBlockHash();
                               return hash;
                           },
                           [](const CTransactionRef &tx) {
                               uint256 id = tx->GetId();
                               return id;
                           },
                       },
                       item);
 }
 
 static bool VerifyProof(const Amount &stakeUtxoDustThreshold,
                         const Proof &proof, bilingual_str &error) {
     ProofValidationState proof_state;
 
     if (!proof.verify(stakeUtxoDustThreshold, proof_state)) {
         switch (proof_state.GetResult()) {
             case ProofValidationResult::NO_STAKE:
                 error = _("The avalanche proof has no stake.");
                 return false;
             case ProofValidationResult::DUST_THRESHOLD:
                 error = _("The avalanche proof stake is too low.");
                 return false;
             case ProofValidationResult::DUPLICATE_STAKE:
                 error = _("The avalanche proof has duplicated stake.");
                 return false;
             case ProofValidationResult::INVALID_STAKE_SIGNATURE:
                 error = _("The avalanche proof has invalid stake signatures.");
                 return false;
             case ProofValidationResult::TOO_MANY_UTXOS:
                 error = strprintf(
                     _("The avalanche proof has too many utxos (max: %u)."),
                     AVALANCHE_MAX_PROOF_STAKES);
                 return false;
             default:
                 error = _("The avalanche proof is invalid.");
                 return false;
         }
     }
 
     return true;
 }
 
 static bool VerifyDelegation(const Delegation &dg,
                              const CPubKey &expectedPubKey,
                              bilingual_str &error) {
     DelegationState dg_state;
 
     CPubKey auth;
     if (!dg.verify(dg_state, auth)) {
         switch (dg_state.GetResult()) {
             case avalanche::DelegationResult::INVALID_SIGNATURE:
                 error = _("The avalanche delegation has invalid signatures.");
                 return false;
             case avalanche::DelegationResult::TOO_MANY_LEVELS:
                 error = _(
                     "The avalanche delegation has too many delegation levels.");
                 return false;
             default:
                 error = _("The avalanche delegation is invalid.");
                 return false;
         }
     }
 
     if (auth != expectedPubKey) {
         error = _(
             "The avalanche delegation does not match the expected public key.");
         return false;
     }
 
     return true;
 }
 
 struct Processor::PeerData {
     ProofRef proof;
     Delegation delegation;
 
     mutable Mutex cs_proofState;
     ProofRegistrationState proofState GUARDED_BY(cs_proofState);
 };
 
 class Processor::NotificationsHandler
     : public interfaces::Chain::Notifications {
     Processor *m_processor;
 
 public:
     NotificationsHandler(Processor *p) : m_processor(p) {}
 
     void updatedBlockTip() override { m_processor->updatedBlockTip(); }
 
     void transactionAddedToMempool(const CTransactionRef &tx,
                                    uint64_t mempool_sequence) override {
         m_processor->transactionAddedToMempool(tx);
     }
 };
 
 Processor::Processor(Config avaconfigIn, interfaces::Chain &chain,
                      CConnman *connmanIn, ChainstateManager &chainmanIn,
                      CTxMemPool *mempoolIn, CScheduler &scheduler,
                      std::unique_ptr<PeerData> peerDataIn, CKey sessionKeyIn,
                      uint32_t minQuorumTotalScoreIn,
                      double minQuorumConnectedScoreRatioIn,
                      int64_t minAvaproofsNodeCountIn,
                      uint32_t staleVoteThresholdIn, uint32_t staleVoteFactorIn,
                      Amount stakeUtxoDustThreshold, bool preConsensus,
                      bool stakingPreConsensus)
     : avaconfig(std::move(avaconfigIn)), connman(connmanIn),
       chainman(chainmanIn), mempool(mempoolIn), round(0),
       peerManager(std::make_unique<PeerManager>(
           stakeUtxoDustThreshold, chainman,
           peerDataIn ? peerDataIn->proof : ProofRef())),
       peerData(std::move(peerDataIn)), sessionKey(std::move(sessionKeyIn)),
       minQuorumScore(minQuorumTotalScoreIn),
       minQuorumConnectedScoreRatio(minQuorumConnectedScoreRatioIn),
       minAvaproofsNodeCount(minAvaproofsNodeCountIn),
       staleVoteThreshold(staleVoteThresholdIn),
       staleVoteFactor(staleVoteFactorIn), m_preConsensus(preConsensus),
       m_stakingPreConsensus(stakingPreConsensus) {
     // Make sure we get notified of chain state changes.
     chainNotificationsHandler =
         chain.handleNotifications(std::make_shared<NotificationsHandler>(this));
 
     scheduler.scheduleEvery(
         [this]() -> bool {
             std::unordered_set<ProofRef, SaltedProofHasher> registeredProofs;
             WITH_LOCK(cs_peerManager,
                       peerManager->cleanupDanglingProofs(registeredProofs));
             for (const auto &proof : registeredProofs) {
                 LogPrint(BCLog::AVALANCHE,
                          "Promoting previously dangling proof %s\n",
                          proof->getId().ToString());
                 reconcileOrFinalize(proof);
             }
             return true;
         },
         5min);
 
     if (!gArgs.GetBoolArg("-persistavapeers", DEFAULT_PERSIST_AVAPEERS)) {
         return;
     }
 
     std::unordered_set<ProofRef, SaltedProofHasher> registeredProofs;
 
     // Attempt to load the peer file if it exists.
     const fs::path dumpPath = gArgs.GetDataDirNet() / AVAPEERS_FILE_NAME;
     WITH_LOCK(cs_peerManager, return peerManager->loadPeersFromFile(
                                   dumpPath, registeredProofs));
 
     // We just loaded the previous finalization status, but make sure to trigger
     // another round of vote for these proofs to avoid issue if the network
     // status changed since the peers file was dumped.
     for (const auto &proof : registeredProofs) {
         addToReconcile(proof);
     }
 
     LogPrint(BCLog::AVALANCHE, "Loaded %d peers from the %s file\n",
              registeredProofs.size(), PathToString(dumpPath));
 }
 
 Processor::~Processor() {
     chainNotificationsHandler.reset();
     stopEventLoop();
 
     if (!gArgs.GetBoolArg("-persistavapeers", DEFAULT_PERSIST_AVAPEERS)) {
         return;
     }
 
     LOCK(cs_peerManager);
     // Discard the status output: if it fails we want to continue normally.
     peerManager->dumpPeersToFile(gArgs.GetDataDirNet() / AVAPEERS_FILE_NAME);
 }
 
 std::unique_ptr<Processor>
 Processor::MakeProcessor(const ArgsManager &argsman, interfaces::Chain &chain,
                          CConnman *connman, ChainstateManager &chainman,
                          CTxMemPool *mempool, CScheduler &scheduler,
                          bilingual_str &error) {
     std::unique_ptr<PeerData> peerData;
     CKey masterKey;
     CKey sessionKey;
 
     Amount stakeUtxoDustThreshold = PROOF_DUST_THRESHOLD;
     if (argsman.IsArgSet("-avaproofstakeutxodustthreshold") &&
         !ParseMoney(argsman.GetArg("-avaproofstakeutxodustthreshold", ""),
                     stakeUtxoDustThreshold)) {
         error = _("The avalanche stake utxo dust threshold amount is invalid.");
         return nullptr;
     }
 
     if (argsman.IsArgSet("-avasessionkey")) {
         sessionKey = DecodeSecret(argsman.GetArg("-avasessionkey", ""));
         if (!sessionKey.IsValid()) {
             error = _("The avalanche session key is invalid.");
             return nullptr;
         }
     } else {
         // Pick a random key for the session.
         sessionKey.MakeNewKey(true);
     }
 
     if (argsman.IsArgSet("-avaproof")) {
         if (!argsman.IsArgSet("-avamasterkey")) {
             error = _(
                 "The avalanche master key is missing for the avalanche proof.");
             return nullptr;
         }
 
         masterKey = DecodeSecret(argsman.GetArg("-avamasterkey", ""));
         if (!masterKey.IsValid()) {
             error = _("The avalanche master key is invalid.");
             return nullptr;
         }
 
         auto proof = RCUPtr<Proof>::make();
         if (!Proof::FromHex(*proof, argsman.GetArg("-avaproof", ""), error)) {
             // error is set by FromHex
             return nullptr;
         }
 
         peerData = std::make_unique<PeerData>();
         peerData->proof = proof;
         if (!VerifyProof(stakeUtxoDustThreshold, *peerData->proof, error)) {
             // error is set by VerifyProof
             return nullptr;
         }
 
         std::unique_ptr<DelegationBuilder> dgb;
         const CPubKey &masterPubKey = masterKey.GetPubKey();
 
         if (argsman.IsArgSet("-avadelegation")) {
             Delegation dg;
             if (!Delegation::FromHex(dg, argsman.GetArg("-avadelegation", ""),
                                      error)) {
                 // error is set by FromHex()
                 return nullptr;
             }
 
             if (dg.getProofId() != peerData->proof->getId()) {
                 error = _("The delegation does not match the proof.");
                 return nullptr;
             }
 
             if (masterPubKey != dg.getDelegatedPubkey()) {
                 error = _(
                     "The master key does not match the delegation public key.");
                 return nullptr;
             }
 
             dgb = std::make_unique<DelegationBuilder>(dg);
         } else {
             if (masterPubKey != peerData->proof->getMaster()) {
                 error =
                     _("The master key does not match the proof public key.");
                 return nullptr;
             }
 
             dgb = std::make_unique<DelegationBuilder>(*peerData->proof);
         }
 
         // Generate the delegation to the session key.
         const CPubKey sessionPubKey = sessionKey.GetPubKey();
         if (sessionPubKey != masterPubKey) {
             if (!dgb->addLevel(masterKey, sessionPubKey)) {
                 error = _("Failed to generate a delegation for this session.");
                 return nullptr;
             }
         }
         peerData->delegation = dgb->build();
 
         if (!VerifyDelegation(peerData->delegation, sessionPubKey, error)) {
             // error is set by VerifyDelegation
             return nullptr;
         }
     }
 
     const auto queryTimeoutDuration =
         std::chrono::milliseconds(argsman.GetIntArg(
             "-avatimeout", AVALANCHE_DEFAULT_QUERY_TIMEOUT.count()));
 
     // Determine quorum parameters
     Amount minQuorumStake = AVALANCHE_DEFAULT_MIN_QUORUM_STAKE;
     if (argsman.IsArgSet("-avaminquorumstake") &&
         !ParseMoney(argsman.GetArg("-avaminquorumstake", ""), minQuorumStake)) {
         error = _("The avalanche min quorum stake amount is invalid.");
         return nullptr;
     }
 
     if (!MoneyRange(minQuorumStake)) {
         error = _("The avalanche min quorum stake amount is out of range.");
         return nullptr;
     }
 
     double minQuorumConnectedStakeRatio =
         AVALANCHE_DEFAULT_MIN_QUORUM_CONNECTED_STAKE_RATIO;
     if (argsman.IsArgSet("-avaminquorumconnectedstakeratio")) {
         // Parse the parameter with a precision of 0.000001.
         int64_t megaMinRatio;
         if (!ParseFixedPoint(
                 argsman.GetArg("-avaminquorumconnectedstakeratio", ""), 6,
                 &megaMinRatio)) {
             error =
                 _("The avalanche min quorum connected stake ratio is invalid.");
             return nullptr;
         }
         minQuorumConnectedStakeRatio = double(megaMinRatio) / 1000000;
     }
 
     if (minQuorumConnectedStakeRatio < 0 || minQuorumConnectedStakeRatio > 1) {
         error = _(
             "The avalanche min quorum connected stake ratio is out of range.");
         return nullptr;
     }
 
     int64_t minAvaproofsNodeCount =
         argsman.GetIntArg("-avaminavaproofsnodecount",
                           AVALANCHE_DEFAULT_MIN_AVAPROOFS_NODE_COUNT);
     if (minAvaproofsNodeCount < 0) {
         error = _("The minimum number of node that sent avaproofs message "
                   "should be non-negative");
         return nullptr;
     }
 
     // Determine voting parameters
     int64_t staleVoteThreshold = argsman.GetIntArg(
         "-avastalevotethreshold", AVALANCHE_VOTE_STALE_THRESHOLD);
     if (staleVoteThreshold < AVALANCHE_VOTE_STALE_MIN_THRESHOLD) {
         error = strprintf(_("The avalanche stale vote threshold must be "
                             "greater than or equal to %d"),
                           AVALANCHE_VOTE_STALE_MIN_THRESHOLD);
         return nullptr;
     }
     if (staleVoteThreshold > std::numeric_limits<uint32_t>::max()) {
         error = strprintf(_("The avalanche stale vote threshold must be less "
                             "than or equal to %d"),
                           std::numeric_limits<uint32_t>::max());
         return nullptr;
     }
 
     int64_t staleVoteFactor =
         argsman.GetIntArg("-avastalevotefactor", AVALANCHE_VOTE_STALE_FACTOR);
     if (staleVoteFactor <= 0) {
         error = _("The avalanche stale vote factor must be greater than 0");
         return nullptr;
     }
     if (staleVoteFactor > std::numeric_limits<uint32_t>::max()) {
         error = strprintf(_("The avalanche stale vote factor must be less than "
                             "or equal to %d"),
                           std::numeric_limits<uint32_t>::max());
         return nullptr;
     }
 
     Config avaconfig(queryTimeoutDuration);
 
     // We can't use std::make_unique with a private constructor
     return std::unique_ptr<Processor>(new Processor(
         std::move(avaconfig), chain, connman, chainman, mempool, scheduler,
         std::move(peerData), std::move(sessionKey),
         Proof::amountToScore(minQuorumStake), minQuorumConnectedStakeRatio,
         minAvaproofsNodeCount, staleVoteThreshold, staleVoteFactor,
         stakeUtxoDustThreshold,
         argsman.GetBoolArg("-avalanchepreconsensus",
                            DEFAULT_AVALANCHE_PRECONSENSUS),
         argsman.GetBoolArg("-avalanchestakingpreconsensus",
                            DEFAULT_AVALANCHE_STAKING_PRECONSENSUS)));
 }
 
 static bool isNull(const AnyVoteItem &item) {
     return item.valueless_by_exception() ||
            std::visit([](const auto &item) { return item == nullptr; }, item);
 };
 
 bool Processor::addToReconcile(const AnyVoteItem &item) {
     if (isNull(item)) {
         return false;
     }
 
     if (!isWorthPolling(item)) {
         return false;
     }
 
     // getLocalAcceptance() takes the voteRecords read lock, so we can't inline
     // the calls or we get a deadlock.
     const bool accepted = getLocalAcceptance(item);
 
     return voteRecords.getWriteView()
         ->insert(std::make_pair(item, VoteRecord(accepted)))
         .second;
 }
 
 bool Processor::reconcileOrFinalize(const ProofRef &proof) {
     if (!proof) {
         return false;
     }
 
     if (isRecentlyFinalized(proof->getId())) {
         PeerId peerid;
         LOCK(cs_peerManager);
         if (peerManager->forPeer(proof->getId(), [&](const Peer &peer) {
                 peerid = peer.peerid;
                 return true;
             })) {
             return peerManager->setFinalized(peerid);
         }
     }
 
     return addToReconcile(proof);
 }
 
 bool Processor::isAccepted(const AnyVoteItem &item) const {
     if (isNull(item)) {
         return false;
     }
 
     auto r = voteRecords.getReadView();
     auto it = r->find(item);
     if (it == r.end()) {
         return false;
     }
 
     return it->second.isAccepted();
 }
 
 int Processor::getConfidence(const AnyVoteItem &item) const {
     if (isNull(item)) {
         return -1;
     }
 
     auto r = voteRecords.getReadView();
     auto it = r->find(item);
     if (it == r.end()) {
         return -1;
     }
 
     return it->second.getConfidence();
 }
 
 bool Processor::isRecentlyFinalized(const uint256 &itemId) const {
     return WITH_LOCK(cs_finalizedItems, return finalizedItems.contains(itemId));
 }
 
 void Processor::clearFinalizedItems() {
     LOCK(cs_finalizedItems);
     finalizedItems.reset();
 }
 
 namespace {
     /**
      * When using TCP, we need to sign all messages as the transport layer is
      * not secure.
      */
     class TCPResponse {
         Response response;
         SchnorrSig sig;
 
     public:
         TCPResponse(Response responseIn, const CKey &key)
             : response(std::move(responseIn)) {
             HashWriter hasher{};
             hasher << response;
             const uint256 hash = hasher.GetHash();
 
             // Now let's sign!
             if (!key.SignSchnorr(hash, sig)) {
                 sig.fill(0);
             }
         }
 
         // serialization support
         SERIALIZE_METHODS(TCPResponse, obj) {
             READWRITE(obj.response, obj.sig);
         }
     };
 } // namespace
 
 void Processor::sendResponse(CNode *pfrom, Response response) const {
     connman->PushMessage(
         pfrom, CNetMsgMaker(pfrom->GetCommonVersion())
                    .Make(NetMsgType::AVARESPONSE,
                          TCPResponse(std::move(response), sessionKey)));
 }
 
 bool Processor::registerVotes(NodeId nodeid, const Response &response,
                               std::vector<VoteItemUpdate> &updates,
                               int &banscore, std::string &error) {
     {
         // Save the time at which we can query again.
         LOCK(cs_peerManager);
 
         // FIXME: This will override the time even when we received an old stale
         // message. This should check that the message is indeed the most up to
         // date one before updating the time.
         peerManager->updateNextRequestTime(
             nodeid, Now<SteadyMilliseconds>() +
                         std::chrono::milliseconds(response.getCooldown()));
     }
 
     std::vector<CInv> invs;
 
     {
         // Check that the query exists. There is a possibility that it has been
         // deleted if the query timed out, so we don't increase the ban score to
         // slowly banning nodes for poor networking over time. Banning has to be
         // handled at callsite to avoid DoS.
         auto w = queries.getWriteView();
         auto it = w->find(std::make_tuple(nodeid, response.getRound()));
         if (it == w.end()) {
             banscore = 0;
             error = "unexpected-ava-response";
             return false;
         }
 
         invs = std::move(it->invs);
         w->erase(it);
     }
 
     // Verify that the request and the vote are consistent.
     const std::vector<Vote> &votes = response.GetVotes();
     size_t size = invs.size();
     if (votes.size() != size) {
         banscore = 100;
         error = "invalid-ava-response-size";
         return false;
     }
 
     for (size_t i = 0; i < size; i++) {
         if (invs[i].hash != votes[i].GetHash()) {
             banscore = 100;
             error = "invalid-ava-response-content";
             return false;
         }
     }
 
     std::map<AnyVoteItem, Vote, VoteMapComparator> responseItems;
 
     // At this stage we are certain that invs[i] matches votes[i], so we can use
     // the inv type to retrieve what is being voted on.
     for (size_t i = 0; i < size; i++) {
         auto item = getVoteItemFromInv(invs[i]);
 
         if (isNull(item)) {
             // This should not happen, but just in case...
             continue;
         }
 
         if (!isWorthPolling(item)) {
             // There is no point polling this item.
             continue;
         }
 
         responseItems.insert(std::make_pair(std::move(item), votes[i]));
     }
 
     auto voteRecordsWriteView = voteRecords.getWriteView();
 
     // Register votes.
     for (const auto &p : responseItems) {
         auto item = p.first;
         const Vote &v = p.second;
 
         auto it = voteRecordsWriteView->find(item);
         if (it == voteRecordsWriteView.end()) {
             // We are not voting on that item anymore.
             continue;
         }
 
         auto &vr = it->second;
         if (!vr.registerVote(nodeid, v.GetError())) {
             if (vr.isStale(staleVoteThreshold, staleVoteFactor)) {
                 updates.emplace_back(std::move(item), VoteStatus::Stale);
 
                 // Just drop stale votes. If we see this item again, we'll
                 // do a new vote.
                 voteRecordsWriteView->erase(it);
             }
             // This vote did not provide any extra information, move on.
             continue;
         }
 
         if (!vr.hasFinalized()) {
             // This item has not been finalized, so we have nothing more to
             // do.
             updates.emplace_back(std::move(item), vr.isAccepted()
                                                       ? VoteStatus::Accepted
                                                       : VoteStatus::Rejected);
             continue;
         }
 
         // We just finalized a vote. If it is valid, then let the caller
         // know. Either way, remove the item from the map.
         updates.emplace_back(std::move(item), vr.isAccepted()
                                                   ? VoteStatus::Finalized
                                                   : VoteStatus::Invalid);
         voteRecordsWriteView->erase(it);
     }
 
     // FIXME This doesn't belong here as it has nothing to do with vote
     // registration.
     for (const auto &update : updates) {
         if (update.getStatus() != VoteStatus::Finalized &&
             update.getStatus() != VoteStatus::Invalid) {
             continue;
         }
 
         const auto &item = update.getVoteItem();
 
         if (update.getStatus() == VoteStatus::Finalized) {
             // Always track finalized items regardless of type. Once finalized
             // they should never become invalid.
             WITH_LOCK(cs_finalizedItems,
                       return finalizedItems.insert(GetVoteItemId(item)));
         }
 
         if (!std::holds_alternative<const CBlockIndex *>(item)) {
             continue;
         }
 
         if (update.getStatus() == VoteStatus::Invalid) {
             // Track invalidated blocks. Other invalidated types are not
             // tracked because they may be rejected for transient reasons
             // (ex: immature proofs or orphaned txs) With blocks this is not
             // the case. A rejected block will not be mined on. To prevent
             // reorgs, invalidated blocks should never be polled again.
             LOCK(cs_invalidatedBlocks);
             invalidatedBlocks.insert(GetVoteItemId(item));
             continue;
         }
 
         // At this point the block index can only be finalized
         const CBlockIndex *pindex = std::get<const CBlockIndex *>(item);
         LOCK(cs_finalizationTip);
         if (finalizationTip &&
             finalizationTip->GetAncestor(pindex->nHeight) == pindex) {
             continue;
         }
 
         finalizationTip = pindex;
     }
 
     return true;
 }
 
 CPubKey Processor::getSessionPubKey() const {
     return sessionKey.GetPubKey();
 }
 
 bool Processor::sendHelloInternal(CNode *pfrom) {
     AssertLockHeld(cs_delayedAvahelloNodeIds);
 
     Delegation delegation;
     if (peerData) {
         if (!canShareLocalProof()) {
             if (!delayedAvahelloNodeIds.emplace(pfrom->GetId()).second) {
                 // Nothing to do
                 return false;
             }
         } else {
             delegation = peerData->delegation;
         }
     }
 
     HashWriter hasher{};
     hasher << delegation.getId();
     hasher << pfrom->GetLocalNonce();
     hasher << pfrom->nRemoteHostNonce;
     hasher << pfrom->GetLocalExtraEntropy();
     hasher << pfrom->nRemoteExtraEntropy;
 
     // Now let's sign!
     SchnorrSig sig;
     if (!sessionKey.SignSchnorr(hasher.GetHash(), sig)) {
         return false;
     }
 
     connman->PushMessage(
         pfrom, CNetMsgMaker(pfrom->GetCommonVersion())
                    .Make(NetMsgType::AVAHELLO, Hello(delegation, sig)));
 
     return delegation.getLimitedProofId() != uint256::ZERO;
 }
 
 bool Processor::sendHello(CNode *pfrom) {
     return WITH_LOCK(cs_delayedAvahelloNodeIds,
                      return sendHelloInternal(pfrom));
 }
 
 void Processor::sendDelayedAvahello() {
     LOCK(cs_delayedAvahelloNodeIds);
 
     auto it = delayedAvahelloNodeIds.begin();
     while (it != delayedAvahelloNodeIds.end()) {
         if (connman->ForNode(*it, [&](CNode *pnode) EXCLUSIVE_LOCKS_REQUIRED(
                                       cs_delayedAvahelloNodeIds) {
                 return sendHelloInternal(pnode);
             })) {
             // Our proof has been announced to this node
             it = delayedAvahelloNodeIds.erase(it);
         } else {
             ++it;
         }
     }
 }
 
 ProofRef Processor::getLocalProof() const {
     return peerData ? peerData->proof : ProofRef();
 }
 
 ProofRegistrationState Processor::getLocalProofRegistrationState() const {
     return peerData
                ? WITH_LOCK(peerData->cs_proofState, return peerData->proofState)
                : ProofRegistrationState();
 }
 
 bool Processor::startEventLoop(CScheduler &scheduler) {
     return eventLoop.startEventLoop(
         scheduler, [this]() { this->runEventLoop(); }, AVALANCHE_TIME_STEP);
 }
 
 bool Processor::stopEventLoop() {
     return eventLoop.stopEventLoop();
 }
 
 void Processor::avaproofsSent(NodeId nodeid) {
     AssertLockNotHeld(cs_main);
 
     if (chainman.ActiveChainstate().IsInitialBlockDownload()) {
         // Before IBD is complete there is no way to make sure a proof is valid
         // or not, e.g. it can be spent in a block we don't know yet. In order
         // to increase confidence that our proof set is similar to other nodes
         // on the network, the messages received during IBD are not accounted.
         return;
     }
 
     LOCK(cs_peerManager);
     if (peerManager->latchAvaproofsSent(nodeid)) {
         avaproofsNodeCounter++;
     }
 }
 
 /*
  * Returns a bool indicating whether we have a usable Avalanche quorum enabling
  * us to take decisions based on polls.
  */
 bool Processor::isQuorumEstablished() {
     AssertLockNotHeld(cs_main);
 
     {
         LOCK(cs_peerManager);
         if (peerManager->getNodeCount() < 8) {
             // There is no point polling if we know the vote cannot converge
             return false;
         }
     }
 
     /*
      * The following parameters can naturally go temporarly below the threshold
      * under normal circumstances, like during a proof replacement with a lower
      * stake amount, or the discovery of a new proofs for which we don't have a
      * node yet.
      * In order to prevent our node from starting and stopping the polls
      * spuriously on such event, the quorum establishement is latched. The only
      * parameters that should not latched is the minimum node count, as this
      * would cause the poll to be inconclusive anyway and should not happen
      * under normal circumstances.
      */
     if (quorumIsEstablished) {
         return true;
     }
 
     // Don't do Avalanche while node is IBD'ing
     if (chainman.ActiveChainstate().IsInitialBlockDownload()) {
         return false;
     }
 
     if (avaproofsNodeCounter < minAvaproofsNodeCount) {
         return false;
     }
 
     auto localProof = getLocalProof();
 
     // Get the registered proof score and registered score we have nodes for
     uint32_t totalPeersScore;
     uint32_t connectedPeersScore;
     {
         LOCK(cs_peerManager);
         totalPeersScore = peerManager->getTotalPeersScore();
         connectedPeersScore = peerManager->getConnectedPeersScore();
 
         // Consider that we are always connected to our proof, even if we are
         // the single node using that proof.
         if (localProof &&
             peerManager->forPeer(localProof->getId(), [](const Peer &peer) {
                 return peer.node_count == 0;
             })) {
             connectedPeersScore += localProof->getScore();
         }
     }
 
     // Ensure enough is being staked overall
     if (totalPeersScore < minQuorumScore) {
         return false;
     }
 
     // Ensure we have connected score for enough of the overall score
     uint32_t minConnectedScore =
         std::round(double(totalPeersScore) * minQuorumConnectedScoreRatio);
     if (connectedPeersScore < minConnectedScore) {
         return false;
     }
 
     quorumIsEstablished = true;
 
     // Attempt to compute the staking rewards winner now so we don't have to
     // wait for a block if we already have all the prerequisites.
     const CBlockIndex *pprev = WITH_LOCK(cs_main, return chainman.ActiveTip());
     if (pprev && IsStakingRewardsActivated(chainman.GetConsensus(), pprev)) {
         computeStakingReward(pprev);
     }
 
     return true;
 }
 
 bool Processor::canShareLocalProof() {
     // The flag is latched
     if (m_canShareLocalProof) {
         return true;
     }
 
     // Don't share our proof if we don't have any inbound connection.
     // This is a best effort measure to prevent advertising a proof if we have
     // limited network connectivity.
     m_canShareLocalProof = connman->GetNodeCount(ConnectionDirection::In) > 0;
 
     return m_canShareLocalProof;
 }
 
 bool Processor::computeStakingReward(const CBlockIndex *pindex) {
     if (!pindex) {
         return false;
     }
 
     // If the quorum is not established there is no point picking a winner that
     // will be rejected.
     if (!isQuorumEstablished()) {
         return false;
     }
 
     {
         LOCK(cs_stakingRewards);
         if (stakingRewards.count(pindex->GetBlockHash()) > 0) {
             return true;
         }
     }
 
     StakingReward _stakingRewards;
     _stakingRewards.blockheight = pindex->nHeight;
 
+    bool rewardsInserted = false;
     if (WITH_LOCK(cs_peerManager, return peerManager->selectStakingRewardWinner(
                                       pindex, _stakingRewards.winners))) {
         LOCK(cs_stakingRewards);
-        return stakingRewards
-            .emplace(pindex->GetBlockHash(), std::move(_stakingRewards))
-            .second;
+        rewardsInserted =
+            stakingRewards
+                .emplace(pindex->GetBlockHash(), std::move(_stakingRewards))
+                .second;
     }
 
-    return false;
+    if (m_stakingPreConsensus) {
+        // If pindex has not been promoted in the contender cache yet, this will
+        // be a no-op.
+        setContenderStatusForLocalWinner(pindex);
+    }
+
+    return rewardsInserted;
 }
 
 bool Processor::eraseStakingRewardWinner(const BlockHash &prevBlockHash) {
     LOCK(cs_stakingRewards);
     return stakingRewards.erase(prevBlockHash) > 0;
 }
 
 void Processor::cleanupStakingRewards(const int minHeight) {
     {
         LOCK(cs_stakingRewards);
         // std::erase_if is only defined since C++20
         for (auto it = stakingRewards.begin(); it != stakingRewards.end();) {
             if (it->second.blockheight < minHeight) {
                 it = stakingRewards.erase(it);
             } else {
                 ++it;
             }
         }
     }
 
     if (m_stakingPreConsensus) {
         WITH_LOCK(cs_stakeContenderCache,
                   return stakeContenderCache.cleanup(minHeight));
     }
 }
 
 bool Processor::getStakingRewardWinners(
     const BlockHash &prevBlockHash,
     std::vector<std::pair<ProofId, CScript>> &winners) const {
     LOCK(cs_stakingRewards);
     auto it = stakingRewards.find(prevBlockHash);
     if (it == stakingRewards.end()) {
         return false;
     }
 
     winners = it->second.winners;
     return true;
 }
 
 bool Processor::getStakingRewardWinners(const BlockHash &prevBlockHash,
                                         std::vector<CScript> &payouts) const {
     std::vector<std::pair<ProofId, CScript>> winners;
     if (!getStakingRewardWinners(prevBlockHash, winners)) {
         return false;
     }
 
     payouts.clear();
     payouts.reserve(winners.size());
     for (auto &winner : winners) {
         payouts.push_back(std::move(winner.second));
     }
 
     return true;
 }
 
 bool Processor::setStakingRewardWinners(const CBlockIndex *pprev,
                                         const std::vector<CScript> &payouts) {
     assert(pprev);
 
     StakingReward stakingReward;
     stakingReward.blockheight = pprev->nHeight;
 
     stakingReward.winners.reserve(payouts.size());
     for (const CScript &payout : payouts) {
         stakingReward.winners.push_back({ProofId(), payout});
     }
 
     if (m_stakingPreConsensus) {
         LOCK(cs_stakeContenderCache);
         stakeContenderCache.setWinners(pprev, payouts);
     }
 
     LOCK(cs_stakingRewards);
     return stakingRewards.insert_or_assign(pprev->GetBlockHash(), stakingReward)
         .second;
 }
 
 void Processor::FinalizeNode(const ::Config &config, const CNode &node) {
     AssertLockNotHeld(cs_main);
 
     const NodeId nodeid = node.GetId();
     WITH_LOCK(cs_peerManager, peerManager->removeNode(nodeid));
     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 {
     BlockHash prevblockhash;
     int status = WITH_LOCK(
         cs_stakeContenderCache,
         return stakeContenderCache.getVoteStatus(contenderId, prevblockhash));
 
     std::vector<std::pair<ProofId, CScript>> winners;
     getStakingRewardWinners(prevblockhash, winners);
 
     if (status != -1 && winners.size() == 0) {
         // If we have not selected a local staking rewards winner yet, indicate
         // this contender is pending to avoid convergence issues.
         return -2;
     }
 
     return status;
 }
 
 void Processor::promoteStakeContendersToTip() {
     const CBlockIndex *activeTip =
         WITH_LOCK(cs_main, return chainman.ActiveTip());
     assert(activeTip);
 
     if (!hasFinalizedTip()) {
         // Avoid growing the contender cache until we have finalized a block
         return;
     }
 
-    LOCK(cs_peerManager);
-    LOCK(cs_stakeContenderCache);
-    stakeContenderCache.promoteToBlock(activeTip, *peerManager);
+    {
+        LOCK(cs_peerManager);
+        LOCK(cs_stakeContenderCache);
+        stakeContenderCache.promoteToBlock(activeTip, *peerManager);
+    }
+
+    // If staking rewards have not been computed yet, we will try again when
+    // they have been.
+    setContenderStatusForLocalWinner(activeTip);
 
     // TODO reconcile remoteProofs contenders
 }
 
+void Processor::setContenderStatusForLocalWinner(const CBlockIndex *pindex) {
+    const BlockHash prevblockhash = pindex->GetBlockHash();
+    std::vector<std::pair<ProofId, CScript>> winners;
+    getStakingRewardWinners(prevblockhash, winners);
+    if (winners.size() == 0) {
+        // Staking rewards not computed yet
+        return;
+    }
+
+    const StakeContenderId contenderId(prevblockhash, winners[0].first);
+    LOCK(cs_stakeContenderCache);
+    stakeContenderCache.finalize(contenderId);
+}
+
 void Processor::updatedBlockTip() {
     const bool registerLocalProof = canShareLocalProof();
     auto registerProofs = [&]() {
         LOCK(cs_peerManager);
 
         auto registeredProofs = peerManager->updatedBlockTip();
 
         ProofRegistrationState localProofState;
         if (peerData && peerData->proof && registerLocalProof) {
             if (peerManager->registerProof(peerData->proof, localProofState)) {
                 registeredProofs.insert(peerData->proof);
             }
 
             if (localProofState.GetResult() ==
                 ProofRegistrationResult::ALREADY_REGISTERED) {
                 // If our proof already exists, that's fine but we don't want to
                 // erase the state with a duplicated proof status, so let's
                 // retrieve the proper state. It also means we are able to
                 // update the status should the proof move from one pool to the
                 // other.
                 const ProofId &localProofId = peerData->proof->getId();
                 if (peerManager->isImmature(localProofId)) {
                     localProofState.Invalid(ProofRegistrationResult::IMMATURE,
                                             "immature-proof");
                 }
                 if (peerManager->isInConflictingPool(localProofId)) {
                     localProofState.Invalid(
                         ProofRegistrationResult::CONFLICTING,
                         "conflicting-utxos");
                 }
                 if (peerManager->isBoundToPeer(localProofId)) {
                     localProofState = ProofRegistrationState();
                 }
             }
 
             WITH_LOCK(peerData->cs_proofState,
                       peerData->proofState = std::move(localProofState));
         }
 
         return registeredProofs;
     };
 
     auto registeredProofs = registerProofs();
     for (const auto &proof : registeredProofs) {
         reconcileOrFinalize(proof);
     }
 
     if (m_stakingPreConsensus) {
         promoteStakeContendersToTip();
     }
 }
 
 void Processor::transactionAddedToMempool(const CTransactionRef &tx) {
     if (m_preConsensus) {
         addToReconcile(tx);
     }
 }
 
 void Processor::runEventLoop() {
     // Don't poll if quorum hasn't been established yet
     if (!isQuorumEstablished()) {
         return;
     }
 
     // First things first, check if we have requests that timed out and clear
     // them.
     clearTimedoutRequests();
 
     // Make sure there is at least one suitable node to query before gathering
     // invs.
     NodeId nodeid = WITH_LOCK(cs_peerManager, return peerManager->selectNode());
     if (nodeid == NO_NODE) {
         return;
     }
     std::vector<CInv> invs = getInvsForNextPoll();
     if (invs.empty()) {
         return;
     }
 
     LOCK(cs_peerManager);
 
     do {
         /**
          * If we lost contact to that node, then we remove it from nodeids, but
          * never add the request to queries, which ensures bad nodes get cleaned
          * up over time.
          */
         bool hasSent = connman->ForNode(
             nodeid, [this, &invs](CNode *pnode) EXCLUSIVE_LOCKS_REQUIRED(
                         cs_peerManager) {
                 uint64_t current_round = round++;
 
                 {
                     // Compute the time at which this requests times out.
                     auto timeout = Now<SteadyMilliseconds>() +
                                    avaconfig.queryTimeoutDuration;
                     // Register the query.
                     queries.getWriteView()->insert(
                         {pnode->GetId(), current_round, timeout, invs});
                     // Set the timeout.
                     peerManager->updateNextRequestTime(pnode->GetId(), timeout);
                 }
 
                 pnode->invsPolled(invs.size());
 
                 // Send the query to the node.
                 connman->PushMessage(
                     pnode, CNetMsgMaker(pnode->GetCommonVersion())
                                .Make(NetMsgType::AVAPOLL,
                                      Poll(current_round, std::move(invs))));
                 return true;
             });
 
         // Success!
         if (hasSent) {
             return;
         }
 
         // This node is obsolete, delete it.
         peerManager->removeNode(nodeid);
 
         // Get next suitable node to try again
         nodeid = peerManager->selectNode();
     } while (nodeid != NO_NODE);
 }
 
 void Processor::clearTimedoutRequests() {
     auto now = Now<SteadyMilliseconds>();
     std::map<CInv, uint8_t> timedout_items{};
 
     {
         // Clear expired requests.
         auto w = queries.getWriteView();
         auto it = w->get<query_timeout>().begin();
         while (it != w->get<query_timeout>().end() && it->timeout < now) {
             for (const auto &i : it->invs) {
                 timedout_items[i]++;
             }
 
             w->get<query_timeout>().erase(it++);
         }
     }
 
     if (timedout_items.empty()) {
         return;
     }
 
     // In flight request accounting.
     auto voteRecordsWriteView = voteRecords.getWriteView();
     for (const auto &p : timedout_items) {
         auto item = getVoteItemFromInv(p.first);
 
         if (isNull(item)) {
             continue;
         }
 
         auto it = voteRecordsWriteView->find(item);
         if (it == voteRecordsWriteView.end()) {
             continue;
         }
 
         it->second.clearInflightRequest(p.second);
     }
 }
 
 std::vector<CInv> Processor::getInvsForNextPoll(bool forPoll) {
     std::vector<CInv> invs;
 
     {
         // First remove all items that are not worth polling.
         auto w = voteRecords.getWriteView();
         for (auto it = w->begin(); it != w->end();) {
             if (!isWorthPolling(it->first)) {
                 it = w->erase(it);
             } else {
                 ++it;
             }
         }
     }
 
     auto buildInvFromVoteItem = variant::overloaded{
         [](const ProofRef &proof) {
             return CInv(MSG_AVA_PROOF, proof->getId());
         },
         [](const CBlockIndex *pindex) {
             return CInv(MSG_BLOCK, pindex->GetBlockHash());
         },
         [](const CTransactionRef &tx) { return CInv(MSG_TX, tx->GetHash()); },
     };
 
     auto r = voteRecords.getReadView();
     for (const auto &[item, voteRecord] : r) {
         if (invs.size() >= AVALANCHE_MAX_ELEMENT_POLL) {
             // Make sure we do not produce more invs than specified by the
             // protocol.
             return invs;
         }
 
         const bool shouldPoll =
             forPoll ? voteRecord.registerPoll() : voteRecord.shouldPoll();
 
         if (!shouldPoll) {
             continue;
         }
 
         invs.emplace_back(std::visit(buildInvFromVoteItem, item));
     }
 
     return invs;
 }
 
 AnyVoteItem Processor::getVoteItemFromInv(const CInv &inv) const {
     if (inv.IsMsgBlk()) {
         return WITH_LOCK(cs_main, return chainman.m_blockman.LookupBlockIndex(
                                       BlockHash(inv.hash)));
     }
 
     if (inv.IsMsgProof()) {
         return WITH_LOCK(cs_peerManager,
                          return peerManager->getProof(ProofId(inv.hash)));
     }
 
     if (mempool && inv.IsMsgTx()) {
         LOCK(mempool->cs);
         if (CTransactionRef tx = mempool->get(TxId(inv.hash))) {
             return tx;
         }
         if (CTransactionRef tx = mempool->withConflicting(
                 [&inv](const TxConflicting &conflicting) {
                     return conflicting.GetTx(TxId(inv.hash));
                 })) {
             return tx;
         }
     }
 
     return {nullptr};
 }
 
 bool Processor::IsWorthPolling::operator()(const CBlockIndex *pindex) const {
     AssertLockNotHeld(cs_main);
 
     LOCK(cs_main);
 
     if (pindex->nStatus.isInvalid()) {
         // No point polling invalid blocks.
         return false;
     }
 
     if (WITH_LOCK(processor.cs_finalizationTip,
                   return processor.finalizationTip &&
                          processor.finalizationTip->GetAncestor(
                              pindex->nHeight) == pindex)) {
         // There is no point polling blocks that are ancestor of a block that
         // has been accepted by the network.
         return false;
     }
 
     if (WITH_LOCK(processor.cs_invalidatedBlocks,
                   return processor.invalidatedBlocks.contains(
                       pindex->GetBlockHash()))) {
         // Blocks invalidated by Avalanche should not be polled twice.
         return false;
     }
 
     return true;
 }
 
 bool Processor::IsWorthPolling::operator()(const ProofRef &proof) const {
     // Avoid lock order issues cs_main -> cs_peerManager
     AssertLockNotHeld(::cs_main);
     AssertLockNotHeld(processor.cs_peerManager);
 
     const ProofId &proofid = proof->getId();
 
     LOCK(processor.cs_peerManager);
 
     // No point polling immature or discarded proofs
     return processor.peerManager->isBoundToPeer(proofid) ||
            processor.peerManager->isInConflictingPool(proofid);
 }
 
 bool Processor::IsWorthPolling::operator()(const CTransactionRef &tx) const {
     if (!processor.mempool) {
         return false;
     }
 
     AssertLockNotHeld(processor.mempool->cs);
     LOCK(processor.mempool->cs);
 
     return processor.mempool->exists(tx->GetId()) ||
            processor.mempool->withConflicting(
                [&tx](const TxConflicting &conflicting) {
                    return conflicting.HaveTx(tx->GetId());
                });
 }
 
 bool Processor::isWorthPolling(const AnyVoteItem &item) const {
     return std::visit(IsWorthPolling(*this), item) &&
            !isRecentlyFinalized(GetVoteItemId(item));
 }
 
 bool Processor::GetLocalAcceptance::operator()(
     const CBlockIndex *pindex) const {
     AssertLockNotHeld(cs_main);
 
     return WITH_LOCK(cs_main,
                      return processor.chainman.ActiveChain().Contains(pindex));
 }
 
 bool Processor::GetLocalAcceptance::operator()(const ProofRef &proof) const {
     AssertLockNotHeld(processor.cs_peerManager);
 
     return WITH_LOCK(
         processor.cs_peerManager,
         return processor.peerManager->isBoundToPeer(proof->getId()));
 }
 
 bool Processor::GetLocalAcceptance::operator()(
     const CTransactionRef &tx) const {
     if (!processor.mempool) {
         return false;
     }
 
     AssertLockNotHeld(processor.mempool->cs);
 
     return WITH_LOCK(processor.mempool->cs,
                      return processor.mempool->exists(tx->GetId()));
 }
 
 } // namespace avalanche
diff --git a/src/avalanche/processor.h b/src/avalanche/processor.h
index 8da35926f..490df4b3d 100644
--- a/src/avalanche/processor.h
+++ b/src/avalanche/processor.h
@@ -1,460 +1,467 @@
 // Copyright (c) 2018-2019 The Bitcoin developers
 // Distributed under the MIT software license, see the accompanying
 // file COPYING or http://www.opensource.org/licenses/mit-license.php.
 
 #ifndef BITCOIN_AVALANCHE_PROCESSOR_H
 #define BITCOIN_AVALANCHE_PROCESSOR_H
 
 #include <avalanche/config.h>
 #include <avalanche/node.h>
 #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>
 #include <common/bloom.h>
 #include <eventloop.h>
 #include <interfaces/chain.h>
 #include <interfaces/handler.h>
 #include <key.h>
 #include <net.h>
 #include <primitives/transaction.h>
 #include <rwcollection.h>
 #include <util/variant.h>
 #include <validationinterface.h>
 
 #include <boost/multi_index/composite_key.hpp>
 #include <boost/multi_index/hashed_index.hpp>
 #include <boost/multi_index/member.hpp>
 #include <boost/multi_index/ordered_index.hpp>
 #include <boost/multi_index_container.hpp>
 
 #include <atomic>
 #include <chrono>
 #include <cstdint>
 #include <memory>
 #include <unordered_map>
 #include <variant>
 #include <vector>
 
 class ArgsManager;
 class CConnman;
 class CNode;
 class CScheduler;
 class Config;
 class PeerManager;
 struct bilingual_str;
 
 /**
  * Maximum item that can be polled at once.
  */
 static constexpr size_t AVALANCHE_MAX_ELEMENT_POLL = 16;
 
 /**
  * How long before we consider that a query timed out.
  */
 static constexpr std::chrono::milliseconds AVALANCHE_DEFAULT_QUERY_TIMEOUT{
     10000};
 
 /**
  * The size of the finalized items filter. It should be large enough that an
  * influx of inventories cannot roll any particular item out of the filter on
  * demand. For example, transactions will roll blocks out of the filter.
  * Tracking many more items than can possibly be polled at once ensures that
  * recently polled items will come to a stable state on the network before
  * rolling out of the filter.
  */
 static constexpr uint32_t AVALANCHE_FINALIZED_ITEMS_FILTER_NUM_ELEMENTS =
     AVALANCHE_MAX_INFLIGHT_POLL * 20;
 
 namespace avalanche {
 
 class Delegation;
 class PeerManager;
 class ProofRegistrationState;
 struct VoteRecord;
 
 enum struct VoteStatus : uint8_t {
     Invalid,
     Rejected,
     Accepted,
     Finalized,
     Stale,
 };
 
 using AnyVoteItem =
     std::variant<const ProofRef, const CBlockIndex *, const CTransactionRef>;
 
 class VoteItemUpdate {
     AnyVoteItem item;
     VoteStatus status;
 
 public:
     VoteItemUpdate(AnyVoteItem itemIn, VoteStatus statusIn)
         : item(std::move(itemIn)), status(statusIn) {}
 
     const VoteStatus &getStatus() const { return status; }
     const AnyVoteItem &getVoteItem() const { return item; }
 };
 
 struct VoteMapComparator {
     bool operator()(const AnyVoteItem &lhs, const AnyVoteItem &rhs) const {
         // If the variants are of different types, sort them by variant index
         if (lhs.index() != rhs.index()) {
             return lhs.index() < rhs.index();
         }
 
         return std::visit(
             variant::overloaded{
                 [](const ProofRef &lhs, const ProofRef &rhs) {
                     return ProofComparatorByScore()(lhs, rhs);
                 },
                 [](const CBlockIndex *lhs, const CBlockIndex *rhs) {
                     // Reverse ordering so we get the highest work first
                     return CBlockIndexWorkComparator()(rhs, lhs);
                 },
                 [](const CTransactionRef &lhs, const CTransactionRef &rhs) {
                     return lhs->GetId() < rhs->GetId();
                 },
                 [](const auto &lhs, const auto &rhs) {
                     // This serves 2 purposes:
                     //  - This makes sure that we don't forget to implement a
                     //    comparison case when adding a new variant type.
                     //  - This avoids having to write all the cross type cases
                     //    which are already handled by the index sort above.
                     //    Because the compiler has no way to determine that, we
                     //    cannot use static assertions here without having to
                     //    define the whole type matrix also.
                     assert(false);
                     // Return any bool, it's only there to make the compiler
                     // happy.
                     return false;
                 },
             },
             lhs, rhs);
     }
 };
 using VoteMap = std::map<AnyVoteItem, VoteRecord, VoteMapComparator>;
 
 struct query_timeout {};
 
 namespace {
     struct AvalancheTest;
 }
 
 // FIXME Implement a proper notification handler for node disconnection instead
 // of implementing the whole NetEventsInterface for a single interesting event.
 class Processor final : public NetEventsInterface {
     Config avaconfig;
     CConnman *connman;
     ChainstateManager &chainman;
     CTxMemPool *mempool;
 
     /**
      * Items to run avalanche on.
      */
     RWCollection<VoteMap> voteRecords;
 
     /**
      * Keep track of peers and queries sent.
      */
     std::atomic<uint64_t> round;
 
     /**
      * Keep track of the peers and associated infos.
      */
     mutable Mutex cs_peerManager;
     std::unique_ptr<PeerManager> peerManager GUARDED_BY(cs_peerManager);
 
     struct Query {
         NodeId nodeid;
         uint64_t round;
         SteadyMilliseconds timeout;
 
         /**
          * We declare this as mutable so it can be modified in the multi_index.
          * This is ok because we do not use this field to index in anyway.
          *
          * /!\ Do not use any mutable field as index.
          */
         mutable std::vector<CInv> invs;
     };
 
     using QuerySet = boost::multi_index_container<
         Query,
         boost::multi_index::indexed_by<
             // index by nodeid/round
             boost::multi_index::hashed_unique<boost::multi_index::composite_key<
                 Query,
                 boost::multi_index::member<Query, NodeId, &Query::nodeid>,
                 boost::multi_index::member<Query, uint64_t, &Query::round>>>,
             // sorted by timeout
             boost::multi_index::ordered_non_unique<
                 boost::multi_index::tag<query_timeout>,
                 boost::multi_index::member<Query, SteadyMilliseconds,
                                            &Query::timeout>>>>;
 
     RWCollection<QuerySet> queries;
 
     /** Data required to participate. */
     struct PeerData;
     std::unique_ptr<PeerData> peerData;
     CKey sessionKey;
 
     /** Event loop machinery. */
     EventLoop eventLoop;
 
     /**
      * Quorum management.
      */
     uint32_t minQuorumScore;
     double minQuorumConnectedScoreRatio;
     std::atomic<bool> quorumIsEstablished{false};
     std::atomic<bool> m_canShareLocalProof{false};
     int64_t minAvaproofsNodeCount;
     std::atomic<int64_t> avaproofsNodeCounter{0};
 
     /** Voting parameters. */
     const uint32_t staleVoteThreshold;
     const uint32_t staleVoteFactor;
 
     /** Registered interfaces::Chain::Notifications handler. */
     class NotificationsHandler;
     std::unique_ptr<interfaces::Handler> chainNotificationsHandler;
 
     mutable Mutex cs_finalizationTip;
     const CBlockIndex *finalizationTip GUARDED_BY(cs_finalizationTip){nullptr};
 
     mutable Mutex cs_delayedAvahelloNodeIds;
     /**
      * A list of the nodes that did not get our proof announced via avahello
      * yet because we had no inbound connection.
      */
     std::unordered_set<NodeId>
         delayedAvahelloNodeIds GUARDED_BY(cs_delayedAvahelloNodeIds);
 
     struct StakingReward {
         int blockheight;
         // Ordered list of acceptable winners, only the first is used for mining
         std::vector<std::pair<ProofId, CScript>> winners;
     };
 
     mutable Mutex cs_stakingRewards;
     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,
               CKey sessionKeyIn, uint32_t minQuorumTotalScoreIn,
               double minQuorumConnectedScoreRatioIn,
               int64_t minAvaproofsNodeCountIn, uint32_t staleVoteThresholdIn,
               uint32_t staleVoteFactorIn, Amount stakeUtxoDustThresholdIn,
               bool preConsensus, bool stakingPreConsensus);
 
 public:
     const bool m_preConsensus{false};
     const bool m_stakingPreConsensus{false};
 
     ~Processor();
 
     static std::unique_ptr<Processor>
     MakeProcessor(const ArgsManager &argsman, interfaces::Chain &chain,
                   CConnman *connman, ChainstateManager &chainman,
                   CTxMemPool *mempoolIn, CScheduler &scheduler,
                   bilingual_str &error);
 
     bool addToReconcile(const AnyVoteItem &item)
         EXCLUSIVE_LOCKS_REQUIRED(!cs_finalizedItems);
     /**
      * Wrapper around the addToReconcile for proofs that adds back the
      * finalization flag to the peer if it is not polled due to being recently
      * finalized.
      */
     bool reconcileOrFinalize(const ProofRef &proof)
         EXCLUSIVE_LOCKS_REQUIRED(!cs_peerManager, !cs_finalizedItems);
     bool isAccepted(const AnyVoteItem &item) const;
     int getConfidence(const AnyVoteItem &item) const;
 
     bool isRecentlyFinalized(const uint256 &itemId) const
         EXCLUSIVE_LOCKS_REQUIRED(!cs_finalizedItems);
     void clearFinalizedItems() EXCLUSIVE_LOCKS_REQUIRED(!cs_finalizedItems);
 
     // TODO: Refactor the API to remove the dependency on avalanche/protocol.h
     void sendResponse(CNode *pfrom, Response response) const;
     bool registerVotes(NodeId nodeid, const Response &response,
                        std::vector<VoteItemUpdate> &updates, int &banscore,
                        std::string &error)
         EXCLUSIVE_LOCKS_REQUIRED(!cs_peerManager, !cs_finalizedItems,
                                  !cs_invalidatedBlocks, !cs_finalizationTip);
 
     template <typename Callable>
     auto withPeerManager(Callable &&func) const
         EXCLUSIVE_LOCKS_REQUIRED(!cs_peerManager) {
         LOCK(cs_peerManager);
         return func(*peerManager);
     }
 
     CPubKey getSessionPubKey() const;
     /**
      * @brief Send a avahello message
      *
      * @param pfrom The node to send the message to
      * @return True if a non-null delegation has been announced
      */
     bool sendHello(CNode *pfrom)
         EXCLUSIVE_LOCKS_REQUIRED(!cs_delayedAvahelloNodeIds);
     void sendDelayedAvahello()
         EXCLUSIVE_LOCKS_REQUIRED(!cs_delayedAvahelloNodeIds);
 
     ProofRef getLocalProof() const;
     ProofRegistrationState getLocalProofRegistrationState() const;
 
     /*
      * Return whether the avalanche service flag should be set.
      */
     bool isAvalancheServiceAvailable() { return !!peerData; }
 
     /** Whether there is a finalized tip */
     bool hasFinalizedTip() const EXCLUSIVE_LOCKS_REQUIRED(!cs_finalizationTip) {
         LOCK(cs_finalizationTip);
         return finalizationTip != nullptr;
     }
 
     bool startEventLoop(CScheduler &scheduler);
     bool stopEventLoop();
 
     void avaproofsSent(NodeId nodeid) LOCKS_EXCLUDED(cs_main)
         EXCLUSIVE_LOCKS_REQUIRED(!cs_peerManager);
     int64_t getAvaproofsNodeCounter() const {
         return avaproofsNodeCounter.load();
     }
     bool isQuorumEstablished() LOCKS_EXCLUDED(cs_main)
-        EXCLUSIVE_LOCKS_REQUIRED(!cs_peerManager, !cs_stakingRewards);
+        EXCLUSIVE_LOCKS_REQUIRED(!cs_peerManager, !cs_stakingRewards,
+                                 !cs_stakeContenderCache);
     bool canShareLocalProof();
 
     bool computeStakingReward(const CBlockIndex *pindex)
-        EXCLUSIVE_LOCKS_REQUIRED(!cs_peerManager, !cs_stakingRewards);
+        EXCLUSIVE_LOCKS_REQUIRED(!cs_peerManager, !cs_stakingRewards,
+                                 !cs_stakeContenderCache);
     bool eraseStakingRewardWinner(const BlockHash &prevBlockHash)
         EXCLUSIVE_LOCKS_REQUIRED(!cs_stakingRewards);
     void cleanupStakingRewards(const int minHeight)
         EXCLUSIVE_LOCKS_REQUIRED(!cs_stakingRewards, !cs_stakeContenderCache);
     bool getStakingRewardWinners(
         const BlockHash &prevBlockHash,
         std::vector<std::pair<ProofId, CScript>> &winners) const
         EXCLUSIVE_LOCKS_REQUIRED(!cs_stakingRewards);
     bool getStakingRewardWinners(const BlockHash &prevBlockHash,
                                  std::vector<CScript> &payouts) const
         EXCLUSIVE_LOCKS_REQUIRED(!cs_stakingRewards);
     bool setStakingRewardWinners(const CBlockIndex *pprev,
                                  const std::vector<CScript> &payouts)
         EXCLUSIVE_LOCKS_REQUIRED(!cs_stakingRewards, !cs_stakeContenderCache);
 
     // Implement NetEventInterface. Only FinalizeNode is of interest.
     void InitializeNode(const ::Config &config, CNode &pnode,
                         ServiceFlags our_services) override {}
     bool ProcessMessages(const ::Config &config, CNode *pnode,
                          std::atomic<bool> &interrupt) override {
         return false;
     }
     bool SendMessages(const ::Config &config, CNode *pnode) override {
         return false;
     }
 
     /** Handle removal of a node */
     void FinalizeNode(const ::Config &config,
                       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, !cs_stakingRewards);
 
     /** Promote stake contender cache entries to the latest chain tip */
     void promoteStakeContendersToTip()
-        EXCLUSIVE_LOCKS_REQUIRED(!cs_stakeContenderCache, !cs_peerManager,
-                                 !cs_finalizationTip);
+        EXCLUSIVE_LOCKS_REQUIRED(!cs_stakeContenderCache, !cs_stakingRewards,
+                                 !cs_peerManager, !cs_finalizationTip);
 
 private:
     void updatedBlockTip()
         EXCLUSIVE_LOCKS_REQUIRED(!cs_peerManager, !cs_finalizedItems,
-                                 !cs_finalizationTip, !cs_stakeContenderCache);
+                                 !cs_finalizationTip, !cs_stakeContenderCache,
+                                 !cs_stakingRewards);
     void transactionAddedToMempool(const CTransactionRef &tx)
         EXCLUSIVE_LOCKS_REQUIRED(!cs_finalizedItems);
     void runEventLoop()
         EXCLUSIVE_LOCKS_REQUIRED(!cs_peerManager, !cs_stakingRewards,
-                                 !cs_finalizedItems);
+                                 !cs_stakeContenderCache, !cs_finalizedItems);
     void clearTimedoutRequests() EXCLUSIVE_LOCKS_REQUIRED(!cs_peerManager);
     std::vector<CInv> getInvsForNextPoll(bool forPoll = true)
         EXCLUSIVE_LOCKS_REQUIRED(!cs_peerManager, !cs_finalizedItems);
     bool sendHelloInternal(CNode *pfrom)
         EXCLUSIVE_LOCKS_REQUIRED(cs_delayedAvahelloNodeIds);
     AnyVoteItem getVoteItemFromInv(const CInv &inv) const
         EXCLUSIVE_LOCKS_REQUIRED(!cs_peerManager);
 
+    /** Helper to set the local winner in the contender cache */
+    void setContenderStatusForLocalWinner(const CBlockIndex *pindex)
+        EXCLUSIVE_LOCKS_REQUIRED(!cs_stakeContenderCache, !cs_stakingRewards);
+
     /**
      * We don't need many blocks but a low false positive rate.
      * In the event of a false positive the node might skip polling this block.
      * Such a block will not get marked as finalized until it is reconsidered
      * for polling (if the filter changed its state) or another block is found.
      */
     mutable Mutex cs_invalidatedBlocks;
     CRollingBloomFilter invalidatedBlocks GUARDED_BY(cs_invalidatedBlocks){
         100, 0.0000001};
 
     /**
      * Rolling bloom filter to track recently finalized inventory items of any
      * type. Once placed in this filter, those items will not be polled again
      * unless they roll out. Note that this one filter tracks all types so
      * blocks may be rolled out by transaction activity for example.
      *
      * We want a low false positive rate to prevent accidentally not polling
      * for an item when it is first seen.
      */
     mutable Mutex cs_finalizedItems;
     CRollingBloomFilter finalizedItems GUARDED_BY(cs_finalizedItems){
         AVALANCHE_FINALIZED_ITEMS_FILTER_NUM_ELEMENTS, 0.0000001};
 
     struct IsWorthPolling {
         const Processor &processor;
 
         IsWorthPolling(const Processor &_processor) : processor(_processor){};
 
         bool operator()(const CBlockIndex *pindex) const
             LOCKS_EXCLUDED(cs_main);
         bool operator()(const ProofRef &proof) const
             LOCKS_EXCLUDED(cs_peerManager);
         bool operator()(const CTransactionRef &tx) const;
     };
     bool isWorthPolling(const AnyVoteItem &item) const
         EXCLUSIVE_LOCKS_REQUIRED(!cs_finalizedItems);
 
     struct GetLocalAcceptance {
         const Processor &processor;
 
         GetLocalAcceptance(const Processor &_processor)
             : processor(_processor){};
 
         bool operator()(const CBlockIndex *pindex) const
             LOCKS_EXCLUDED(cs_main);
         bool operator()(const ProofRef &proof) const
             LOCKS_EXCLUDED(cs_peerManager);
         bool operator()(const CTransactionRef &tx) const;
     };
     bool getLocalAcceptance(const AnyVoteItem &item) const {
         return std::visit(GetLocalAcceptance(*this), item);
     }
 
     friend struct ::avalanche::AvalancheTest;
 };
 
 } // namespace avalanche
 
 #endif // BITCOIN_AVALANCHE_PROCESSOR_H
diff --git a/src/avalanche/test/processor_tests.cpp b/src/avalanche/test/processor_tests.cpp
index 5819b987a..6151d6934 100644
--- a/src/avalanche/test/processor_tests.cpp
+++ b/src/avalanche/test/processor_tests.cpp
@@ -1,2563 +1,2560 @@
 // Copyright (c) 2018-2020 The Bitcoin developers
 // Distributed under the MIT software license, see the accompanying
 // file COPYING or http://www.opensource.org/licenses/mit-license.php.
 
 #include <avalanche/processor.h>
 
 #include <arith_uint256.h>
 #include <avalanche/avalanche.h>
 #include <avalanche/delegationbuilder.h>
 #include <avalanche/peermanager.h>
 #include <avalanche/proofbuilder.h>
 #include <avalanche/voterecord.h>
 #include <chain.h>
 #include <config.h>
 #include <core_io.h>
 #include <key_io.h>
 #include <net_processing.h> // For ::PeerManager
 #include <reverse_iterator.h>
 #include <scheduler.h>
 #include <util/time.h>
 #include <util/translation.h> // For bilingual_str
 
 #include <avalanche/test/util.h>
 #include <test/util/setup_common.h>
 
 #include <boost/mpl/list.hpp>
 #include <boost/test/unit_test.hpp>
 
 #include <functional>
 #include <limits>
 #include <type_traits>
 #include <vector>
 
 using namespace avalanche;
 
 namespace avalanche {
 namespace {
     struct AvalancheTest {
         static void runEventLoop(avalanche::Processor &p) { p.runEventLoop(); }
 
         static std::vector<CInv> getInvsForNextPoll(Processor &p) {
             return p.getInvsForNextPoll(false);
         }
 
         static NodeId getSuitableNodeToQuery(Processor &p) {
             return WITH_LOCK(p.cs_peerManager,
                              return p.peerManager->selectNode());
         }
 
         static uint64_t getRound(const Processor &p) { return p.round; }
 
         static uint32_t getMinQuorumScore(const Processor &p) {
             return p.minQuorumScore;
         }
 
         static double getMinQuorumConnectedScoreRatio(const Processor &p) {
             return p.minQuorumConnectedScoreRatio;
         }
 
         static void clearavaproofsNodeCounter(Processor &p) {
             p.avaproofsNodeCounter = 0;
         }
 
         static void addVoteRecord(Processor &p, AnyVoteItem &item,
                                   VoteRecord &voteRecord) {
             p.voteRecords.getWriteView()->insert(
                 std::make_pair(item, voteRecord));
         }
 
         static void setFinalizationTip(Processor &p,
                                        const CBlockIndex *pindex) {
             LOCK(p.cs_finalizationTip);
             p.finalizationTip = pindex;
         }
 
         static void setLocalProofShareable(Processor &p, bool shareable) {
             p.m_canShareLocalProof = shareable;
         }
 
         static void updatedBlockTip(Processor &p) { p.updatedBlockTip(); }
 
         static void addProofToRecentfinalized(Processor &p,
                                               const ProofId &proofid) {
             WITH_LOCK(p.cs_finalizedItems,
                       return p.finalizedItems.insert(proofid));
         }
     };
 } // namespace
 
 struct TestVoteRecord : public VoteRecord {
     explicit TestVoteRecord(uint16_t conf) : VoteRecord(true) {
         confidence |= conf << 1;
     }
 };
 } // namespace avalanche
 
 namespace {
 struct CConnmanTest : public CConnman {
     using CConnman::CConnman;
     void AddNode(CNode &node) {
         LOCK(m_nodes_mutex);
         m_nodes.push_back(&node);
     }
     void ClearNodes() {
         LOCK(m_nodes_mutex);
         for (CNode *node : m_nodes) {
             delete node;
         }
         m_nodes.clear();
     }
 };
 
 CService ip(uint32_t i) {
     struct in_addr s;
     s.s_addr = i;
     return CService(CNetAddr(s), Params().GetDefaultPort());
 }
 
 struct AvalancheTestingSetup : public TestChain100Setup {
     const ::Config &config;
     CConnmanTest *m_connman;
 
     std::unique_ptr<Processor> m_processor;
 
     // The master private key we delegate to.
     CKey masterpriv;
 
     std::unordered_set<std::string> m_overridden_args;
 
     AvalancheTestingSetup()
         : TestChain100Setup(), config(GetConfig()),
           masterpriv(CKey::MakeCompressedKey()) {
         // Deterministic randomness for tests.
         auto connman = std::make_unique<CConnmanTest>(config, 0x1337, 0x1337,
                                                       *m_node.addrman);
         m_connman = connman.get();
         m_node.connman = std::move(connman);
 
         // Get the processor ready.
         setArg("-avaminquorumstake", "0");
         setArg("-avaminquorumconnectedstakeratio", "0");
         setArg("-avaminavaproofsnodecount", "0");
         setArg("-avaproofstakeutxoconfirmations", "1");
         bilingual_str error;
         m_processor = Processor::MakeProcessor(
             *m_node.args, *m_node.chain, m_node.connman.get(),
             *Assert(m_node.chainman), m_node.mempool.get(), *m_node.scheduler,
             error);
         BOOST_CHECK(m_processor);
 
         m_node.peerman = ::PeerManager::make(
             *m_connman, *m_node.addrman, m_node.banman.get(), *m_node.chainman,
             *m_node.mempool, m_processor.get(), {});
         m_node.chain = interfaces::MakeChain(m_node, config.GetChainParams());
     }
 
     ~AvalancheTestingSetup() {
         m_connman->ClearNodes();
         SyncWithValidationInterfaceQueue();
 
         ArgsManager &argsman = *Assert(m_node.args);
         for (const std::string &key : m_overridden_args) {
             argsman.ClearForcedArg(key);
         }
         m_overridden_args.clear();
     }
 
     CNode *ConnectNode(ServiceFlags nServices) {
         static NodeId id = 0;
 
         CAddress addr(ip(GetRand<uint32_t>()), NODE_NONE);
         auto node =
             new CNode(id++, /*sock=*/nullptr, addr,
                       /* nKeyedNetGroupIn */ 0,
                       /* nLocalHostNonceIn */ 0,
                       /* nLocalExtraEntropyIn */ 0, CAddress(),
                       /* pszDest */ "", ConnectionType::OUTBOUND_FULL_RELAY,
                       /* inbound_onion */ false);
         node->SetCommonVersion(PROTOCOL_VERSION);
         node->m_has_all_wanted_services =
             HasAllDesirableServiceFlags(nServices);
         m_node.peerman->InitializeNode(config, *node, NODE_NETWORK);
         node->nVersion = 1;
         node->fSuccessfullyConnected = true;
 
         m_connman->AddNode(*node);
         return node;
     }
 
     ProofRef GetProof(CScript payoutScript = UNSPENDABLE_ECREG_PAYOUT_SCRIPT) {
         const CKey key = CKey::MakeCompressedKey();
         const COutPoint outpoint{TxId(GetRandHash()), 0};
         CScript script = GetScriptForDestination(PKHash(key.GetPubKey()));
         const Amount amount = PROOF_DUST_THRESHOLD;
         const uint32_t height = 100;
 
         LOCK(cs_main);
         CCoinsViewCache &coins =
             Assert(m_node.chainman)->ActiveChainstate().CoinsTip();
         coins.AddCoin(outpoint, Coin(CTxOut(amount, script), height, false),
                       false);
 
         ProofBuilder pb(0, 0, masterpriv, payoutScript);
         BOOST_CHECK(pb.addUTXO(outpoint, amount, height, false, key));
         return pb.build();
     }
 
     bool addNode(NodeId nodeid, const ProofId &proofid) {
         return m_processor->withPeerManager([&](avalanche::PeerManager &pm) {
             return pm.addNode(nodeid, proofid);
         });
     }
 
     bool addNode(NodeId nodeid) {
         auto proof = GetProof();
         return m_processor->withPeerManager([&](avalanche::PeerManager &pm) {
             return pm.registerProof(proof) &&
                    pm.addNode(nodeid, proof->getId());
         });
     }
 
     std::array<CNode *, 8> ConnectNodes() {
         auto proof = GetProof();
         BOOST_CHECK(
             m_processor->withPeerManager([&](avalanche::PeerManager &pm) {
                 return pm.registerProof(proof);
             }));
         const ProofId &proofid = proof->getId();
 
         std::array<CNode *, 8> nodes;
         for (CNode *&n : nodes) {
             n = ConnectNode(NODE_AVALANCHE);
             BOOST_CHECK(addNode(n->GetId(), proofid));
         }
 
         return nodes;
     }
 
     void runEventLoop() { AvalancheTest::runEventLoop(*m_processor); }
 
     NodeId getSuitableNodeToQuery() {
         return AvalancheTest::getSuitableNodeToQuery(*m_processor);
     }
 
     std::vector<CInv> getInvsForNextPoll() {
         return AvalancheTest::getInvsForNextPoll(*m_processor);
     }
 
     uint64_t getRound() const { return AvalancheTest::getRound(*m_processor); }
 
     bool registerVotes(NodeId nodeid, const avalanche::Response &response,
                        std::vector<avalanche::VoteItemUpdate> &updates,
                        std::string &error) {
         int banscore;
         return m_processor->registerVotes(nodeid, response, updates, banscore,
                                           error);
     }
 
     bool registerVotes(NodeId nodeid, const avalanche::Response &response,
                        std::vector<avalanche::VoteItemUpdate> &updates) {
         int banscore;
         std::string error;
         return m_processor->registerVotes(nodeid, response, updates, banscore,
                                           error);
     }
 
     void setArg(std::string key, const std::string &value) {
         ArgsManager &argsman = *Assert(m_node.args);
         argsman.ForceSetArg(key, value);
         m_overridden_args.emplace(std::move(key));
     }
 
     bool addToReconcile(const AnyVoteItem &item) {
         return m_processor->addToReconcile(item);
     }
 };
 
 struct BlockProvider {
     AvalancheTestingSetup *fixture;
     uint32_t invType;
 
     BlockProvider(AvalancheTestingSetup *_fixture)
         : fixture(_fixture), invType(MSG_BLOCK) {}
 
     CBlockIndex *buildVoteItem() const {
         CBlock block = fixture->CreateAndProcessBlock({}, CScript());
         const BlockHash blockHash = block.GetHash();
 
         LOCK(cs_main);
         return Assert(fixture->m_node.chainman)
             ->m_blockman.LookupBlockIndex(blockHash);
     }
 
     uint256 getVoteItemId(const CBlockIndex *pindex) const {
         return pindex->GetBlockHash();
     }
 
     std::vector<Vote> buildVotesForItems(uint32_t error,
                                          std::vector<CBlockIndex *> &&items) {
         size_t numItems = items.size();
 
         std::vector<Vote> votes;
         votes.reserve(numItems);
 
         // Votes are sorted by most work first
         std::sort(items.begin(), items.end(), CBlockIndexWorkComparator());
         for (auto &item : reverse_iterate(items)) {
             votes.emplace_back(error, item->GetBlockHash());
         }
 
         return votes;
     }
 
     void invalidateItem(CBlockIndex *pindex) {
         LOCK(::cs_main);
         pindex->nStatus = pindex->nStatus.withFailed();
     }
 
     const CBlockIndex *fromAnyVoteItem(const AnyVoteItem &item) {
         return std::get<const CBlockIndex *>(item);
     }
 };
 
 struct ProofProvider {
     AvalancheTestingSetup *fixture;
     uint32_t invType;
 
     ProofProvider(AvalancheTestingSetup *_fixture)
         : fixture(_fixture), invType(MSG_AVA_PROOF) {}
 
     ProofRef buildVoteItem() const {
         const ProofRef proof = fixture->GetProof();
         fixture->m_processor->withPeerManager([&](avalanche::PeerManager &pm) {
             BOOST_CHECK(pm.registerProof(proof));
         });
         return proof;
     }
 
     uint256 getVoteItemId(const ProofRef &proof) const {
         return proof->getId();
     }
 
     std::vector<Vote> buildVotesForItems(uint32_t error,
                                          std::vector<ProofRef> &&items) {
         size_t numItems = items.size();
 
         std::vector<Vote> votes;
         votes.reserve(numItems);
 
         // Votes are sorted by high score first
         std::sort(items.begin(), items.end(), ProofComparatorByScore());
         for (auto &item : items) {
             votes.emplace_back(error, item->getId());
         }
 
         return votes;
     }
 
     void invalidateItem(const ProofRef &proof) {
         fixture->m_processor->withPeerManager([&](avalanche::PeerManager &pm) {
             pm.rejectProof(proof->getId(),
                            avalanche::PeerManager::RejectionMode::INVALIDATE);
         });
     }
 
     const ProofRef fromAnyVoteItem(const AnyVoteItem &item) {
         return std::get<const ProofRef>(item);
     }
 };
 
 struct TxProvider {
     AvalancheTestingSetup *fixture;
 
     std::vector<avalanche::VoteItemUpdate> updates;
     uint32_t invType;
 
     TxProvider(AvalancheTestingSetup *_fixture)
         : fixture(_fixture), invType(MSG_TX) {}
 
     CTransactionRef buildVoteItem() const {
         CMutableTransaction mtx;
         mtx.nVersion = 2;
         mtx.vin.emplace_back(COutPoint{TxId(FastRandomContext().rand256()), 0});
         mtx.vout.emplace_back(1 * COIN, CScript() << OP_TRUE);
 
         CTransactionRef tx = MakeTransactionRef(std::move(mtx));
 
         TestMemPoolEntryHelper mempoolEntryHelper;
         auto entry = mempoolEntryHelper.FromTx(tx);
 
         CTxMemPool *mempool = Assert(fixture->m_node.mempool.get());
         {
             LOCK2(cs_main, mempool->cs);
             mempool->addUnchecked(entry);
             BOOST_CHECK(mempool->exists(tx->GetId()));
         }
 
         return tx;
     }
 
     uint256 getVoteItemId(const CTransactionRef &tx) const {
         return tx->GetId();
     }
 
     std::vector<Vote> buildVotesForItems(uint32_t error,
                                          std::vector<CTransactionRef> &&items) {
         size_t numItems = items.size();
 
         std::vector<Vote> votes;
         votes.reserve(numItems);
 
         // Transactions are sorted by TxId
         std::sort(items.begin(), items.end(),
                   [](const CTransactionRef &lhs, const CTransactionRef &rhs) {
                       return lhs->GetId() < rhs->GetId();
                   });
         for (auto &item : items) {
             votes.emplace_back(error, item->GetId());
         }
 
         return votes;
     }
 
     void invalidateItem(const CTransactionRef &tx) {
         BOOST_CHECK(tx != nullptr);
         CTxMemPool *mempool = Assert(fixture->m_node.mempool.get());
 
         LOCK(mempool->cs);
         mempool->removeRecursive(*tx, MemPoolRemovalReason::CONFLICT);
         BOOST_CHECK(!mempool->exists(tx->GetId()));
     }
 
     const CTransactionRef fromAnyVoteItem(const AnyVoteItem &item) {
         return std::get<const CTransactionRef>(item);
     }
 };
 
 } // namespace
 
 BOOST_FIXTURE_TEST_SUITE(processor_tests, AvalancheTestingSetup)
 
 // FIXME A std::tuple can be used instead of boost::mpl::list after boost 1.67
 using VoteItemProviders =
     boost::mpl::list<BlockProvider, ProofProvider, TxProvider>;
 
 BOOST_AUTO_TEST_CASE_TEMPLATE(voteitemupdate, P, VoteItemProviders) {
     P provider(this);
 
     std::set<VoteStatus> status{
         VoteStatus::Invalid,   VoteStatus::Rejected, VoteStatus::Accepted,
         VoteStatus::Finalized, VoteStatus::Stale,
     };
 
     auto item = provider.buildVoteItem();
 
     for (auto s : status) {
         VoteItemUpdate itemUpdate(item, s);
         // The use of BOOST_CHECK instead of BOOST_CHECK_EQUAL prevents from
         // having to define operator<<() for each argument type.
         BOOST_CHECK(provider.fromAnyVoteItem(itemUpdate.getVoteItem()) == item);
         BOOST_CHECK(itemUpdate.getStatus() == s);
     }
 }
 
 namespace {
 Response next(Response &r) {
     auto copy = r;
     r = {r.getRound() + 1, r.getCooldown(), r.GetVotes()};
     return copy;
 }
 } // namespace
 
 BOOST_AUTO_TEST_CASE_TEMPLATE(item_reconcile_twice, P, VoteItemProviders) {
     P provider(this);
     ChainstateManager &chainman = *Assert(m_node.chainman);
     const CBlockIndex *chaintip =
         WITH_LOCK(chainman.GetMutex(), return chainman.ActiveTip());
 
     auto item = provider.buildVoteItem();
     auto itemid = provider.getVoteItemId(item);
 
     // Adding the item twice does nothing.
     BOOST_CHECK(addToReconcile(item));
     BOOST_CHECK(!addToReconcile(item));
     BOOST_CHECK(m_processor->isAccepted(item));
 
     // Create nodes that supports avalanche so we can finalize the item.
     auto avanodes = ConnectNodes();
 
     int nextNodeIndex = 0;
     std::vector<avalanche::VoteItemUpdate> updates;
     auto registerNewVote = [&](const Response &resp) {
         runEventLoop();
         auto nodeid = avanodes[nextNodeIndex++ % avanodes.size()]->GetId();
         BOOST_CHECK(registerVotes(nodeid, resp, updates));
     };
 
     // Finalize the item.
     auto finalize = [&](const auto finalizeItemId) {
         Response resp = {getRound(), 0, {Vote(0, finalizeItemId)}};
         for (int i = 0; i < AVALANCHE_FINALIZATION_SCORE + 6; i++) {
             registerNewVote(next(resp));
             if (updates.size() > 0) {
                 break;
             }
         }
         BOOST_CHECK_EQUAL(updates.size(), 1);
         BOOST_CHECK(updates[0].getStatus() == VoteStatus::Finalized);
         updates.clear();
     };
     finalize(itemid);
 
     // The finalized item cannot be reconciled for a while.
     BOOST_CHECK(!addToReconcile(item));
 
     auto finalizeNewItem = [&]() {
         auto anotherItem = provider.buildVoteItem();
         AnyVoteItem anotherVoteItem = AnyVoteItem(anotherItem);
         auto anotherItemId = provider.getVoteItemId(anotherItem);
 
         TestVoteRecord voteRecord(AVALANCHE_FINALIZATION_SCORE - 1);
         AvalancheTest::addVoteRecord(*m_processor, anotherVoteItem, voteRecord);
         finalize(anotherItemId);
     };
 
     // The filter can have new items added up to its size and the item will
     // still not reconcile.
     for (uint32_t i = 0; i < AVALANCHE_FINALIZED_ITEMS_FILTER_NUM_ELEMENTS;
          i++) {
         finalizeNewItem();
         BOOST_CHECK(!addToReconcile(item));
     }
 
     // But if we keep going it will eventually roll out of the filter and can
     // be reconciled again.
     for (uint32_t i = 0; i < AVALANCHE_FINALIZED_ITEMS_FILTER_NUM_ELEMENTS;
          i++) {
         finalizeNewItem();
     }
 
     // Roll back the finalization point so that reconciling the old block does
     // not fail the finalization check. This is a no-op for other types.
     AvalancheTest::setFinalizationTip(*m_processor, chaintip);
 
     BOOST_CHECK(addToReconcile(item));
 }
 
 BOOST_AUTO_TEST_CASE_TEMPLATE(item_null, P, VoteItemProviders) {
     P provider(this);
 
     // Check that null case is handled on the public interface
     BOOST_CHECK(!m_processor->isAccepted(nullptr));
     BOOST_CHECK_EQUAL(m_processor->getConfidence(nullptr), -1);
 
     auto item = decltype(provider.buildVoteItem())();
     BOOST_CHECK(item == nullptr);
     BOOST_CHECK(!addToReconcile(item));
 
     // Check that adding item to vote on doesn't change the outcome. A
     // comparator is used under the hood, and this is skipped if there are no
     // vote records.
     item = provider.buildVoteItem();
     BOOST_CHECK(addToReconcile(item));
 
     BOOST_CHECK(!m_processor->isAccepted(nullptr));
     BOOST_CHECK_EQUAL(m_processor->getConfidence(nullptr), -1);
 }
 
 BOOST_AUTO_TEST_CASE_TEMPLATE(vote_item_register, P, VoteItemProviders) {
     P provider(this);
     const uint32_t invType = provider.invType;
 
     auto item = provider.buildVoteItem();
     auto itemid = provider.getVoteItemId(item);
 
     // Create nodes that supports avalanche.
     auto avanodes = ConnectNodes();
 
     // Querying for random item returns false.
     BOOST_CHECK(!m_processor->isAccepted(item));
 
     // Add a new item. Check it is added to the polls.
     BOOST_CHECK(addToReconcile(item));
     auto invs = getInvsForNextPoll();
     BOOST_CHECK_EQUAL(invs.size(), 1);
     BOOST_CHECK_EQUAL(invs[0].type, invType);
     BOOST_CHECK(invs[0].hash == itemid);
 
     BOOST_CHECK(m_processor->isAccepted(item));
 
     int nextNodeIndex = 0;
     std::vector<avalanche::VoteItemUpdate> updates;
     auto registerNewVote = [&](const Response &resp) {
         runEventLoop();
         auto nodeid = avanodes[nextNodeIndex++ % avanodes.size()]->GetId();
         BOOST_CHECK(registerVotes(nodeid, resp, updates));
     };
 
     // Let's vote for this item a few times.
     Response resp{0, 0, {Vote(0, itemid)}};
     for (int i = 0; i < 6; i++) {
         registerNewVote(next(resp));
         BOOST_CHECK(m_processor->isAccepted(item));
         BOOST_CHECK_EQUAL(m_processor->getConfidence(item), 0);
         BOOST_CHECK_EQUAL(updates.size(), 0);
     }
 
     // A single neutral vote do not change anything.
     resp = {getRound(), 0, {Vote(-1, itemid)}};
     registerNewVote(next(resp));
     BOOST_CHECK(m_processor->isAccepted(item));
     BOOST_CHECK_EQUAL(m_processor->getConfidence(item), 0);
     BOOST_CHECK_EQUAL(updates.size(), 0);
 
     resp = {getRound(), 0, {Vote(0, itemid)}};
     for (int i = 1; i < 7; i++) {
         registerNewVote(next(resp));
         BOOST_CHECK(m_processor->isAccepted(item));
         BOOST_CHECK_EQUAL(m_processor->getConfidence(item), i);
         BOOST_CHECK_EQUAL(updates.size(), 0);
     }
 
     // Two neutral votes will stall progress.
     resp = {getRound(), 0, {Vote(-1, itemid)}};
     registerNewVote(next(resp));
     BOOST_CHECK(m_processor->isAccepted(item));
     BOOST_CHECK_EQUAL(m_processor->getConfidence(item), 6);
     BOOST_CHECK_EQUAL(updates.size(), 0);
     registerNewVote(next(resp));
     BOOST_CHECK(m_processor->isAccepted(item));
     BOOST_CHECK_EQUAL(m_processor->getConfidence(item), 6);
     BOOST_CHECK_EQUAL(updates.size(), 0);
 
     resp = {getRound(), 0, {Vote(0, itemid)}};
     for (int i = 2; i < 8; i++) {
         registerNewVote(next(resp));
         BOOST_CHECK(m_processor->isAccepted(item));
         BOOST_CHECK_EQUAL(m_processor->getConfidence(item), 6);
         BOOST_CHECK_EQUAL(updates.size(), 0);
     }
 
     // We vote for it numerous times to finalize it.
     for (int i = 7; i < AVALANCHE_FINALIZATION_SCORE; i++) {
         registerNewVote(next(resp));
         BOOST_CHECK(m_processor->isAccepted(item));
         BOOST_CHECK_EQUAL(m_processor->getConfidence(item), i);
         BOOST_CHECK_EQUAL(updates.size(), 0);
     }
 
     // As long as it is not finalized, we poll.
     invs = getInvsForNextPoll();
     BOOST_CHECK_EQUAL(invs.size(), 1);
     BOOST_CHECK_EQUAL(invs[0].type, invType);
     BOOST_CHECK(invs[0].hash == itemid);
 
     // Now finalize the decision.
     registerNewVote(next(resp));
     BOOST_CHECK_EQUAL(updates.size(), 1);
     BOOST_CHECK(provider.fromAnyVoteItem(updates[0].getVoteItem()) == item);
     BOOST_CHECK(updates[0].getStatus() == VoteStatus::Finalized);
     updates.clear();
 
     // Once the decision is finalized, there is no poll for it.
     invs = getInvsForNextPoll();
     BOOST_CHECK_EQUAL(invs.size(), 0);
 
     // Get a new item to vote on
     item = provider.buildVoteItem();
     itemid = provider.getVoteItemId(item);
     BOOST_CHECK(addToReconcile(item));
 
     // Now let's finalize rejection.
     invs = getInvsForNextPoll();
     BOOST_CHECK_EQUAL(invs.size(), 1);
     BOOST_CHECK_EQUAL(invs[0].type, invType);
     BOOST_CHECK(invs[0].hash == itemid);
 
     resp = {getRound(), 0, {Vote(1, itemid)}};
     for (int i = 0; i < 6; i++) {
         registerNewVote(next(resp));
         BOOST_CHECK(m_processor->isAccepted(item));
         BOOST_CHECK_EQUAL(updates.size(), 0);
     }
 
     // Now the state will flip.
     registerNewVote(next(resp));
     BOOST_CHECK(!m_processor->isAccepted(item));
     BOOST_CHECK_EQUAL(updates.size(), 1);
     BOOST_CHECK(provider.fromAnyVoteItem(updates[0].getVoteItem()) == item);
     BOOST_CHECK(updates[0].getStatus() == VoteStatus::Rejected);
     updates.clear();
 
     // Now it is rejected, but we can vote for it numerous times.
     for (int i = 1; i < AVALANCHE_FINALIZATION_SCORE; i++) {
         registerNewVote(next(resp));
         BOOST_CHECK(!m_processor->isAccepted(item));
         BOOST_CHECK_EQUAL(updates.size(), 0);
     }
 
     // As long as it is not finalized, we poll.
     invs = getInvsForNextPoll();
     BOOST_CHECK_EQUAL(invs.size(), 1);
     BOOST_CHECK_EQUAL(invs[0].type, invType);
     BOOST_CHECK(invs[0].hash == itemid);
 
     // Now finalize the decision.
     registerNewVote(next(resp));
     BOOST_CHECK(!m_processor->isAccepted(item));
     BOOST_CHECK_EQUAL(updates.size(), 1);
     BOOST_CHECK(provider.fromAnyVoteItem(updates[0].getVoteItem()) == item);
     BOOST_CHECK(updates[0].getStatus() == VoteStatus::Invalid);
     updates.clear();
 
     // Once the decision is finalized, there is no poll for it.
     invs = getInvsForNextPoll();
     BOOST_CHECK_EQUAL(invs.size(), 0);
 }
 
 BOOST_AUTO_TEST_CASE_TEMPLATE(multi_item_register, P, VoteItemProviders) {
     P provider(this);
     const uint32_t invType = provider.invType;
 
     auto itemA = provider.buildVoteItem();
     auto itemidA = provider.getVoteItemId(itemA);
 
     auto itemB = provider.buildVoteItem();
     auto itemidB = provider.getVoteItemId(itemB);
 
     // Create several nodes that support avalanche.
     auto avanodes = ConnectNodes();
 
     // Querying for random item returns false.
     BOOST_CHECK(!m_processor->isAccepted(itemA));
     BOOST_CHECK(!m_processor->isAccepted(itemB));
 
     // Start voting on item A.
     BOOST_CHECK(addToReconcile(itemA));
     auto invs = getInvsForNextPoll();
     BOOST_CHECK_EQUAL(invs.size(), 1);
     BOOST_CHECK_EQUAL(invs[0].type, invType);
     BOOST_CHECK(invs[0].hash == itemidA);
 
     uint64_t round = getRound();
     runEventLoop();
     std::vector<avalanche::VoteItemUpdate> updates;
     BOOST_CHECK(registerVotes(avanodes[0]->GetId(),
                               {round, 0, {Vote(0, itemidA)}}, updates));
     BOOST_CHECK_EQUAL(updates.size(), 0);
 
     // Start voting on item B after one vote.
     std::vector<Vote> votes = provider.buildVotesForItems(0, {itemA, itemB});
     Response resp{round + 1, 0, votes};
     BOOST_CHECK(addToReconcile(itemB));
     invs = getInvsForNextPoll();
     BOOST_CHECK_EQUAL(invs.size(), 2);
 
     // Ensure the inv ordering is as expected
     for (size_t i = 0; i < invs.size(); i++) {
         BOOST_CHECK_EQUAL(invs[i].type, invType);
         BOOST_CHECK(invs[i].hash == votes[i].GetHash());
     }
 
     // Let's vote for these items a few times.
     for (int i = 0; i < 4; i++) {
         NodeId nodeid = getSuitableNodeToQuery();
         runEventLoop();
         BOOST_CHECK(registerVotes(nodeid, next(resp), updates));
         BOOST_CHECK_EQUAL(updates.size(), 0);
     }
 
     // Now it is accepted, but we can vote for it numerous times.
     for (int i = 0; i < AVALANCHE_FINALIZATION_SCORE; i++) {
         NodeId nodeid = getSuitableNodeToQuery();
         runEventLoop();
         BOOST_CHECK(registerVotes(nodeid, next(resp), updates));
         BOOST_CHECK_EQUAL(updates.size(), 0);
     }
 
     // Running two iterration of the event loop so that vote gets triggered on A
     // and B.
     NodeId firstNodeid = getSuitableNodeToQuery();
     runEventLoop();
     NodeId secondNodeid = getSuitableNodeToQuery();
     runEventLoop();
 
     BOOST_CHECK(firstNodeid != secondNodeid);
 
     // Next vote will finalize item A.
     BOOST_CHECK(registerVotes(firstNodeid, next(resp), updates));
     BOOST_CHECK_EQUAL(updates.size(), 1);
     BOOST_CHECK(provider.fromAnyVoteItem(updates[0].getVoteItem()) == itemA);
     BOOST_CHECK(updates[0].getStatus() == VoteStatus::Finalized);
     updates.clear();
 
     // We do not vote on A anymore.
     invs = getInvsForNextPoll();
     BOOST_CHECK_EQUAL(invs.size(), 1);
     BOOST_CHECK_EQUAL(invs[0].type, invType);
     BOOST_CHECK(invs[0].hash == itemidB);
 
     // Next vote will finalize item B.
     BOOST_CHECK(registerVotes(secondNodeid, resp, updates));
     BOOST_CHECK_EQUAL(updates.size(), 1);
     BOOST_CHECK(provider.fromAnyVoteItem(updates[0].getVoteItem()) == itemB);
     BOOST_CHECK(updates[0].getStatus() == VoteStatus::Finalized);
     updates.clear();
 
     // There is nothing left to vote on.
     invs = getInvsForNextPoll();
     BOOST_CHECK_EQUAL(invs.size(), 0);
 }
 
 BOOST_AUTO_TEST_CASE_TEMPLATE(poll_and_response, P, VoteItemProviders) {
     P provider(this);
     const uint32_t invType = provider.invType;
 
     auto item = provider.buildVoteItem();
     auto itemid = provider.getVoteItemId(item);
 
     // There is no node to query.
     BOOST_CHECK_EQUAL(getSuitableNodeToQuery(), NO_NODE);
 
     // Add enough nodes to have a valid quorum, and the same amount with no
     // avalanche support
     std::set<NodeId> avanodeIds;
     auto avanodes = ConnectNodes();
     for (auto avanode : avanodes) {
         ConnectNode(NODE_NONE);
         avanodeIds.insert(avanode->GetId());
     }
 
     auto getSelectedAvanodeId = [&]() {
         NodeId avanodeid = getSuitableNodeToQuery();
         BOOST_CHECK(avanodeIds.find(avanodeid) != avanodeIds.end());
         return avanodeid;
     };
 
     // It returns one of the avalanche peer.
     NodeId avanodeid = getSelectedAvanodeId();
 
     // Register an item and check it is added to the list of elements to poll.
     BOOST_CHECK(addToReconcile(item));
     auto invs = getInvsForNextPoll();
     BOOST_CHECK_EQUAL(invs.size(), 1);
     BOOST_CHECK_EQUAL(invs[0].type, invType);
     BOOST_CHECK(invs[0].hash == itemid);
 
     std::set<NodeId> unselectedNodeids = avanodeIds;
     unselectedNodeids.erase(avanodeid);
     const size_t remainingNodeIds = unselectedNodeids.size();
 
     uint64_t round = getRound();
     for (size_t i = 0; i < remainingNodeIds; i++) {
         // Trigger a poll on avanode.
         runEventLoop();
 
         // Another node is selected
         NodeId nodeid = getSuitableNodeToQuery();
         BOOST_CHECK(unselectedNodeids.find(nodeid) != avanodeIds.end());
         unselectedNodeids.erase(nodeid);
     }
 
     // There is no more suitable peer available, so return nothing.
     BOOST_CHECK(unselectedNodeids.empty());
     runEventLoop();
     BOOST_CHECK_EQUAL(getSuitableNodeToQuery(), NO_NODE);
 
     // Respond to the request.
     Response resp = {round, 0, {Vote(0, itemid)}};
     std::vector<avalanche::VoteItemUpdate> updates;
     BOOST_CHECK(registerVotes(avanodeid, resp, updates));
     BOOST_CHECK_EQUAL(updates.size(), 0);
 
     // Now that avanode fullfilled his request, it is added back to the list of
     // queriable nodes.
     BOOST_CHECK_EQUAL(getSuitableNodeToQuery(), avanodeid);
 
     auto checkRegisterVotesError = [&](NodeId nodeid,
                                        const avalanche::Response &response,
                                        const std::string &expectedError) {
         std::string error;
         BOOST_CHECK(!registerVotes(nodeid, response, updates, error));
         BOOST_CHECK_EQUAL(error, expectedError);
         BOOST_CHECK_EQUAL(updates.size(), 0);
     };
 
     // Sending a response when not polled fails.
     checkRegisterVotesError(avanodeid, next(resp), "unexpected-ava-response");
 
     // Trigger a poll on avanode.
     round = getRound();
     runEventLoop();
     BOOST_CHECK_EQUAL(getSuitableNodeToQuery(), NO_NODE);
 
     // Sending responses that do not match the request also fails.
     // 1. Too many results.
     resp = {round, 0, {Vote(0, itemid), Vote(0, itemid)}};
     runEventLoop();
     checkRegisterVotesError(avanodeid, resp, "invalid-ava-response-size");
     BOOST_CHECK_EQUAL(getSuitableNodeToQuery(), avanodeid);
 
     // 2. Not enough results.
     resp = {getRound(), 0, {}};
     runEventLoop();
     checkRegisterVotesError(avanodeid, resp, "invalid-ava-response-size");
     BOOST_CHECK_EQUAL(getSuitableNodeToQuery(), avanodeid);
 
     // 3. Do not match the poll.
     resp = {getRound(), 0, {Vote()}};
     runEventLoop();
     checkRegisterVotesError(avanodeid, resp, "invalid-ava-response-content");
     BOOST_CHECK_EQUAL(getSuitableNodeToQuery(), avanodeid);
 
     // At this stage we have reached the max inflight requests for our inv, so
     // it won't be requested anymore until the requests are fullfilled. Let's
     // vote on another item with no inflight request so the remaining tests
     // makes sense.
     invs = getInvsForNextPoll();
     BOOST_CHECK(invs.empty());
 
     item = provider.buildVoteItem();
     itemid = provider.getVoteItemId(item);
     BOOST_CHECK(addToReconcile(item));
 
     invs = getInvsForNextPoll();
     BOOST_CHECK_EQUAL(invs.size(), 1);
 
     // 4. Invalid round count. Request is not discarded.
     uint64_t queryRound = getRound();
     runEventLoop();
 
     resp = {queryRound + 1, 0, {Vote()}};
     checkRegisterVotesError(avanodeid, resp, "unexpected-ava-response");
 
     resp = {queryRound - 1, 0, {Vote()}};
     checkRegisterVotesError(avanodeid, resp, "unexpected-ava-response");
 
     // 5. Making request for invalid nodes do not work. Request is not
     // discarded.
     resp = {queryRound, 0, {Vote(0, itemid)}};
     checkRegisterVotesError(avanodeid + 1234, resp, "unexpected-ava-response");
 
     // Proper response gets processed and avanode is available again.
     resp = {queryRound, 0, {Vote(0, itemid)}};
     BOOST_CHECK(registerVotes(avanodeid, resp, updates));
     BOOST_CHECK_EQUAL(updates.size(), 0);
     BOOST_CHECK_EQUAL(getSuitableNodeToQuery(), avanodeid);
 
     // Out of order response are rejected.
     const auto item2 = provider.buildVoteItem();
     BOOST_CHECK(addToReconcile(item2));
 
     std::vector<Vote> votes = provider.buildVotesForItems(0, {item, item2});
     resp = {getRound(), 0, {votes[1], votes[0]}};
     runEventLoop();
     checkRegisterVotesError(avanodeid, resp, "invalid-ava-response-content");
     BOOST_CHECK_EQUAL(getSuitableNodeToQuery(), avanodeid);
 
     // But they are accepted in order.
     resp = {getRound(), 0, votes};
     runEventLoop();
     BOOST_CHECK(registerVotes(avanodeid, resp, updates));
     BOOST_CHECK_EQUAL(updates.size(), 0);
     BOOST_CHECK_EQUAL(getSuitableNodeToQuery(), avanodeid);
 }
 
 BOOST_AUTO_TEST_CASE_TEMPLATE(dont_poll_invalid_item, P, VoteItemProviders) {
     P provider(this);
     const uint32_t invType = provider.invType;
 
     auto itemA = provider.buildVoteItem();
     auto itemB = provider.buildVoteItem();
 
     auto avanodes = ConnectNodes();
 
     // Build votes to get proper ordering
     std::vector<Vote> votes = provider.buildVotesForItems(0, {itemA, itemB});
 
     // Register the items and check they are added to the list of elements to
     // poll.
     BOOST_CHECK(addToReconcile(itemA));
     BOOST_CHECK(addToReconcile(itemB));
     auto invs = getInvsForNextPoll();
     BOOST_CHECK_EQUAL(invs.size(), 2);
     for (size_t i = 0; i < invs.size(); i++) {
         BOOST_CHECK_EQUAL(invs[i].type, invType);
         BOOST_CHECK(invs[i].hash == votes[i].GetHash());
     }
 
     // When an item is marked invalid, stop polling.
     provider.invalidateItem(itemB);
 
     Response goodResp{getRound(), 0, {Vote(0, provider.getVoteItemId(itemA))}};
     std::vector<avalanche::VoteItemUpdate> updates;
     runEventLoop();
     BOOST_CHECK(registerVotes(avanodes[0]->GetId(), goodResp, updates));
     BOOST_CHECK_EQUAL(updates.size(), 0);
 
     // Votes including itemB are rejected
     Response badResp{getRound(), 0, votes};
     runEventLoop();
     std::string error;
     BOOST_CHECK(!registerVotes(avanodes[1]->GetId(), badResp, updates, error));
     BOOST_CHECK_EQUAL(error, "invalid-ava-response-size");
 }
 
 BOOST_TEST_DECORATOR(*boost::unit_test::timeout(60))
 BOOST_AUTO_TEST_CASE_TEMPLATE(poll_inflight_timeout, P, VoteItemProviders) {
     P provider(this);
     ChainstateManager &chainman = *Assert(m_node.chainman);
 
     auto queryTimeDuration = std::chrono::milliseconds(10);
     setArg("-avatimeout", ToString(queryTimeDuration.count()));
 
     bilingual_str error;
     m_processor = Processor::MakeProcessor(
         *m_node.args, *m_node.chain, m_node.connman.get(), chainman,
         m_node.mempool.get(), *m_node.scheduler, error);
 
     const auto item = provider.buildVoteItem();
     const auto itemid = provider.getVoteItemId(item);
 
     // Add the item
     BOOST_CHECK(addToReconcile(item));
 
     // Create a quorum of nodes that support avalanche.
     ConnectNodes();
     NodeId avanodeid = NO_NODE;
 
     // Expire requests after some time.
     for (int i = 0; i < 10; i++) {
         Response resp = {getRound(), 0, {Vote(0, itemid)}};
         avanodeid = getSuitableNodeToQuery();
 
         auto start = Now<SteadyMilliseconds>();
         runEventLoop();
         // We cannot guarantee that we'll wait for just 1ms, so we have to bail
         // if we aren't within the proper time range.
         std::this_thread::sleep_for(std::chrono::milliseconds(1));
         runEventLoop();
 
         std::vector<avalanche::VoteItemUpdate> updates;
         bool ret = registerVotes(avanodeid, next(resp), updates);
         if (Now<SteadyMilliseconds>() > start + queryTimeDuration) {
             // We waited for too long, bail. Because we can't know for sure when
             // previous steps ran, ret is not deterministic and we do not check
             // it.
             i--;
             continue;
         }
 
         // We are within time bounds, so the vote should have worked.
         BOOST_CHECK(ret);
 
         avanodeid = getSuitableNodeToQuery();
 
         // Now try again but wait for expiration.
         runEventLoop();
         std::this_thread::sleep_for(queryTimeDuration);
         runEventLoop();
         BOOST_CHECK(!registerVotes(avanodeid, next(resp), updates));
     }
 }
 
 BOOST_AUTO_TEST_CASE_TEMPLATE(poll_inflight_count, P, VoteItemProviders) {
     P provider(this);
     const uint32_t invType = provider.invType;
 
     // Create enough nodes so that we run into the inflight request limit.
     auto proof = GetProof();
     BOOST_CHECK(m_processor->withPeerManager(
         [&](avalanche::PeerManager &pm) { return pm.registerProof(proof); }));
 
     std::array<CNode *, AVALANCHE_MAX_INFLIGHT_POLL + 1> nodes;
     for (auto &n : nodes) {
         n = ConnectNode(NODE_AVALANCHE);
         BOOST_CHECK(addNode(n->GetId(), proof->getId()));
     }
 
     // Add an item to poll
     const auto item = provider.buildVoteItem();
     const auto itemid = provider.getVoteItemId(item);
     BOOST_CHECK(addToReconcile(item));
 
     // Ensure there are enough requests in flight.
     std::map<NodeId, uint64_t> node_round_map;
     for (int i = 0; i < AVALANCHE_MAX_INFLIGHT_POLL; i++) {
         NodeId nodeid = getSuitableNodeToQuery();
         BOOST_CHECK(node_round_map.find(nodeid) == node_round_map.end());
         node_round_map.insert(std::pair<NodeId, uint64_t>(nodeid, getRound()));
         auto invs = getInvsForNextPoll();
         BOOST_CHECK_EQUAL(invs.size(), 1);
         BOOST_CHECK_EQUAL(invs[0].type, invType);
         BOOST_CHECK(invs[0].hash == itemid);
         runEventLoop();
     }
 
     // Now that we have enough in flight requests, we shouldn't poll.
     auto suitablenodeid = getSuitableNodeToQuery();
     BOOST_CHECK(suitablenodeid != NO_NODE);
     auto invs = getInvsForNextPoll();
     BOOST_CHECK_EQUAL(invs.size(), 0);
     runEventLoop();
     BOOST_CHECK_EQUAL(getSuitableNodeToQuery(), suitablenodeid);
 
     // Send one response, now we can poll again.
     auto it = node_round_map.begin();
     Response resp = {it->second, 0, {Vote(0, itemid)}};
     std::vector<avalanche::VoteItemUpdate> updates;
     BOOST_CHECK(registerVotes(it->first, resp, updates));
     node_round_map.erase(it);
 
     invs = getInvsForNextPoll();
     BOOST_CHECK_EQUAL(invs.size(), 1);
     BOOST_CHECK_EQUAL(invs[0].type, invType);
     BOOST_CHECK(invs[0].hash == itemid);
 }
 
 BOOST_AUTO_TEST_CASE(quorum_diversity) {
     std::vector<VoteItemUpdate> updates;
 
     CBlock block = CreateAndProcessBlock({}, CScript());
     const BlockHash blockHash = block.GetHash();
     const CBlockIndex *pindex;
     {
         LOCK(cs_main);
         pindex =
             Assert(m_node.chainman)->m_blockman.LookupBlockIndex(blockHash);
     }
 
     // Create nodes that supports avalanche.
     auto avanodes = ConnectNodes();
 
     // Querying for random block returns false.
     BOOST_CHECK(!m_processor->isAccepted(pindex));
 
     // Add a new block. Check it is added to the polls.
     BOOST_CHECK(m_processor->addToReconcile(pindex));
 
     // Do one valid round of voting.
     uint64_t round = getRound();
     Response resp{round, 0, {Vote(0, blockHash)}};
 
     // Check that all nodes can vote.
     for (size_t i = 0; i < avanodes.size(); i++) {
         runEventLoop();
         BOOST_CHECK(registerVotes(avanodes[i]->GetId(), next(resp), updates));
     }
 
     // Generate a query for every single node.
     const NodeId firstNodeId = getSuitableNodeToQuery();
     std::map<NodeId, uint64_t> node_round_map;
     round = getRound();
     for (size_t i = 0; i < avanodes.size(); i++) {
         NodeId nodeid = getSuitableNodeToQuery();
         BOOST_CHECK(node_round_map.find(nodeid) == node_round_map.end());
         node_round_map[nodeid] = getRound();
         runEventLoop();
     }
 
     // Now only the first node can vote. All others would be duplicate in the
     // quorum.
     auto confidence = m_processor->getConfidence(pindex);
     BOOST_REQUIRE(confidence > 0);
 
     for (auto &[nodeid, r] : node_round_map) {
         if (nodeid == firstNodeId) {
             // Node 0 is the only one which can vote at this stage.
             round = r;
             continue;
         }
 
         BOOST_CHECK(
             registerVotes(nodeid, {r, 0, {Vote(0, blockHash)}}, updates));
         BOOST_CHECK_EQUAL(m_processor->getConfidence(pindex), confidence);
     }
 
     BOOST_CHECK(
         registerVotes(firstNodeId, {round, 0, {Vote(0, blockHash)}}, updates));
     BOOST_CHECK_EQUAL(m_processor->getConfidence(pindex), confidence + 1);
 }
 
 BOOST_AUTO_TEST_CASE(event_loop) {
     CScheduler s;
 
     CBlock block = CreateAndProcessBlock({}, CScript());
     const BlockHash blockHash = block.GetHash();
     const CBlockIndex *pindex;
     {
         LOCK(cs_main);
         pindex =
             Assert(m_node.chainman)->m_blockman.LookupBlockIndex(blockHash);
     }
 
     // Starting the event loop.
     BOOST_CHECK(m_processor->startEventLoop(s));
 
     // There is one task planned in the next hour (our event loop).
     std::chrono::steady_clock::time_point start, stop;
     BOOST_CHECK_EQUAL(s.getQueueInfo(start, stop), 1);
 
     // Starting twice doesn't start it twice.
     BOOST_CHECK(!m_processor->startEventLoop(s));
 
     // Start the scheduler thread.
     std::thread schedulerThread(std::bind(&CScheduler::serviceQueue, &s));
 
     // Create a quorum of nodes that support avalanche.
     auto avanodes = ConnectNodes();
 
     // There is no query in flight at the moment.
     NodeId nodeid = getSuitableNodeToQuery();
     BOOST_CHECK_NE(nodeid, NO_NODE);
 
     // Add a new block. Check it is added to the polls.
     uint64_t queryRound = getRound();
     BOOST_CHECK(m_processor->addToReconcile(pindex));
 
     // Wait until all nodes got a poll
     for (int i = 0; i < 60 * 1000; i++) {
         // Technically, this is a race condition, but this should do just fine
         // as we wait up to 1 minute for an event that should take 80ms.
         UninterruptibleSleep(std::chrono::milliseconds(1));
         if (getRound() == queryRound + avanodes.size()) {
             break;
         }
     }
 
     // Check that we effectively got a request and not timed out.
     BOOST_CHECK(getRound() > queryRound);
 
     // Respond and check the cooldown time is respected.
     uint64_t responseRound = getRound();
     auto queryTime = Now<SteadyMilliseconds>() + std::chrono::milliseconds(100);
 
     std::vector<VoteItemUpdate> updates;
     // Only the first node answers, so it's the only one that gets polled again
     BOOST_CHECK(registerVotes(nodeid, {queryRound, 100, {Vote(0, blockHash)}},
                               updates));
 
     for (int i = 0; i < 10000; i++) {
         // We make sure that we do not get a request before queryTime.
         UninterruptibleSleep(std::chrono::milliseconds(1));
         if (getRound() != responseRound) {
             BOOST_CHECK(Now<SteadyMilliseconds>() >= queryTime);
             break;
         }
     }
 
     // But we eventually get one.
     BOOST_CHECK(getRound() > responseRound);
 
     // Stop event loop.
     BOOST_CHECK(m_processor->stopEventLoop());
 
     // We don't have any task scheduled anymore.
     BOOST_CHECK_EQUAL(s.getQueueInfo(start, stop), 0);
 
     // Can't stop the event loop twice.
     BOOST_CHECK(!m_processor->stopEventLoop());
 
     // Wait for the scheduler to stop.
     s.StopWhenDrained();
     schedulerThread.join();
 }
 
 BOOST_AUTO_TEST_CASE(destructor) {
     CScheduler s;
     std::chrono::steady_clock::time_point start, stop;
 
     std::thread schedulerThread;
     BOOST_CHECK(m_processor->startEventLoop(s));
     BOOST_CHECK_EQUAL(s.getQueueInfo(start, stop), 1);
 
     // Start the service thread after the queue size check to prevent a race
     // condition where the thread may be processing the event loop task during
     // the check.
     schedulerThread = std::thread(std::bind(&CScheduler::serviceQueue, &s));
 
     // Destroy the processor.
     m_processor.reset();
 
     // Now that avalanche is destroyed, there is no more scheduled tasks.
     BOOST_CHECK_EQUAL(s.getQueueInfo(start, stop), 0);
 
     // Wait for the scheduler to stop.
     s.StopWhenDrained();
     schedulerThread.join();
 }
 
 BOOST_AUTO_TEST_CASE(add_proof_to_reconcile) {
     uint32_t score = MIN_VALID_PROOF_SCORE;
     Chainstate &active_chainstate = Assert(m_node.chainman)->ActiveChainstate();
 
     auto addProofToReconcile = [&](uint32_t proofScore) {
         auto proof = buildRandomProof(active_chainstate, proofScore);
         m_processor->withPeerManager([&](avalanche::PeerManager &pm) {
             BOOST_CHECK(pm.registerProof(proof));
         });
         BOOST_CHECK(m_processor->addToReconcile(proof));
         return proof;
     };
 
     for (size_t i = 0; i < AVALANCHE_MAX_ELEMENT_POLL; i++) {
         auto proof = addProofToReconcile(++score);
 
         auto invs = AvalancheTest::getInvsForNextPoll(*m_processor);
         BOOST_CHECK_EQUAL(invs.size(), i + 1);
         BOOST_CHECK(invs.front().IsMsgProof());
         BOOST_CHECK_EQUAL(invs.front().hash, proof->getId());
     }
 
     // From here a new proof is only polled if its score is in the top
     // AVALANCHE_MAX_ELEMENT_POLL
     ProofId lastProofId;
     for (size_t i = 0; i < 10; i++) {
         auto proof = addProofToReconcile(++score);
 
         auto invs = AvalancheTest::getInvsForNextPoll(*m_processor);
         BOOST_CHECK_EQUAL(invs.size(), AVALANCHE_MAX_ELEMENT_POLL);
         BOOST_CHECK(invs.front().IsMsgProof());
         BOOST_CHECK_EQUAL(invs.front().hash, proof->getId());
 
         lastProofId = proof->getId();
     }
 
     for (size_t i = 0; i < 10; i++) {
         auto proof = addProofToReconcile(--score);
 
         auto invs = AvalancheTest::getInvsForNextPoll(*m_processor);
         BOOST_CHECK_EQUAL(invs.size(), AVALANCHE_MAX_ELEMENT_POLL);
         BOOST_CHECK(invs.front().IsMsgProof());
         BOOST_CHECK_EQUAL(invs.front().hash, lastProofId);
     }
 
     {
         // The score is not high enough to get polled
         auto proof = addProofToReconcile(--score);
         auto invs = AvalancheTest::getInvsForNextPoll(*m_processor);
         for (auto &inv : invs) {
             BOOST_CHECK_NE(inv.hash, proof->getId());
         }
     }
 }
 
 BOOST_AUTO_TEST_CASE(proof_record) {
     setArg("-avaproofstakeutxoconfirmations", "2");
     setArg("-avalancheconflictingproofcooldown", "0");
 
     BOOST_CHECK(!m_processor->isAccepted(nullptr));
     BOOST_CHECK_EQUAL(m_processor->getConfidence(nullptr), -1);
 
     const CKey key = CKey::MakeCompressedKey();
 
     const COutPoint conflictingOutpoint{TxId(GetRandHash()), 0};
     const COutPoint immatureOutpoint{TxId(GetRandHash()), 0};
     {
         CScript script = GetScriptForDestination(PKHash(key.GetPubKey()));
 
         LOCK(cs_main);
         CCoinsViewCache &coins =
             Assert(m_node.chainman)->ActiveChainstate().CoinsTip();
         coins.AddCoin(conflictingOutpoint,
                       Coin(CTxOut(PROOF_DUST_THRESHOLD, script), 10, false),
                       false);
         coins.AddCoin(immatureOutpoint,
                       Coin(CTxOut(PROOF_DUST_THRESHOLD, script), 100, false),
                       false);
     }
 
     auto buildProof = [&](const COutPoint &outpoint, uint64_t sequence,
                           uint32_t height = 10) {
         ProofBuilder pb(sequence, 0, key, UNSPENDABLE_ECREG_PAYOUT_SCRIPT);
         BOOST_CHECK(
             pb.addUTXO(outpoint, PROOF_DUST_THRESHOLD, height, false, key));
         return pb.build();
     };
 
     auto conflictingProof = buildProof(conflictingOutpoint, 1);
     auto validProof = buildProof(conflictingOutpoint, 2);
     auto immatureProof = buildProof(immatureOutpoint, 3, 100);
 
     BOOST_CHECK(!m_processor->isAccepted(conflictingProof));
     BOOST_CHECK(!m_processor->isAccepted(validProof));
     BOOST_CHECK(!m_processor->isAccepted(immatureProof));
     BOOST_CHECK_EQUAL(m_processor->getConfidence(conflictingProof), -1);
     BOOST_CHECK_EQUAL(m_processor->getConfidence(validProof), -1);
     BOOST_CHECK_EQUAL(m_processor->getConfidence(immatureProof), -1);
 
     // Reconciling proofs that don't exist will fail
     BOOST_CHECK(!m_processor->addToReconcile(conflictingProof));
     BOOST_CHECK(!m_processor->addToReconcile(validProof));
     BOOST_CHECK(!m_processor->addToReconcile(immatureProof));
 
     m_processor->withPeerManager([&](avalanche::PeerManager &pm) {
         BOOST_CHECK(pm.registerProof(conflictingProof));
         BOOST_CHECK(pm.registerProof(validProof));
         BOOST_CHECK(!pm.registerProof(immatureProof));
 
         BOOST_CHECK(pm.isBoundToPeer(validProof->getId()));
         BOOST_CHECK(pm.isInConflictingPool(conflictingProof->getId()));
         BOOST_CHECK(pm.isImmature(immatureProof->getId()));
     });
 
     BOOST_CHECK(m_processor->addToReconcile(conflictingProof));
     BOOST_CHECK(!m_processor->isAccepted(conflictingProof));
     BOOST_CHECK(!m_processor->isAccepted(validProof));
     BOOST_CHECK(!m_processor->isAccepted(immatureProof));
     BOOST_CHECK_EQUAL(m_processor->getConfidence(conflictingProof), 0);
     BOOST_CHECK_EQUAL(m_processor->getConfidence(validProof), -1);
     BOOST_CHECK_EQUAL(m_processor->getConfidence(immatureProof), -1);
 
     BOOST_CHECK(m_processor->addToReconcile(validProof));
     BOOST_CHECK(!m_processor->isAccepted(conflictingProof));
     BOOST_CHECK(m_processor->isAccepted(validProof));
     BOOST_CHECK(!m_processor->isAccepted(immatureProof));
     BOOST_CHECK_EQUAL(m_processor->getConfidence(conflictingProof), 0);
     BOOST_CHECK_EQUAL(m_processor->getConfidence(validProof), 0);
     BOOST_CHECK_EQUAL(m_processor->getConfidence(immatureProof), -1);
 
     BOOST_CHECK(!m_processor->addToReconcile(immatureProof));
     BOOST_CHECK(!m_processor->isAccepted(conflictingProof));
     BOOST_CHECK(m_processor->isAccepted(validProof));
     BOOST_CHECK(!m_processor->isAccepted(immatureProof));
     BOOST_CHECK_EQUAL(m_processor->getConfidence(conflictingProof), 0);
     BOOST_CHECK_EQUAL(m_processor->getConfidence(validProof), 0);
     BOOST_CHECK_EQUAL(m_processor->getConfidence(immatureProof), -1);
 }
 
 BOOST_AUTO_TEST_CASE(quorum_detection) {
     // Set min quorum parameters for our test
     int minStake = 400'000'000;
     setArg("-avaminquorumstake", ToString(minStake));
     setArg("-avaminquorumconnectedstakeratio", "0.5");
 
     // Create a new processor with our given quorum parameters
     const auto currency = Currency::get();
     uint32_t minScore = Proof::amountToScore(minStake * currency.baseunit);
 
     Chainstate &active_chainstate = Assert(m_node.chainman)->ActiveChainstate();
 
     const CKey key = CKey::MakeCompressedKey();
     auto localProof =
         buildRandomProof(active_chainstate, minScore / 4, 100, key);
     setArg("-avamasterkey", EncodeSecret(key));
     setArg("-avaproof", localProof->ToHex());
 
     bilingual_str error;
     ChainstateManager &chainman = *Assert(m_node.chainman);
     m_processor = Processor::MakeProcessor(
         *m_node.args, *m_node.chain, m_node.connman.get(), chainman,
         m_node.mempool.get(), *m_node.scheduler, error);
 
     BOOST_CHECK(m_processor != nullptr);
     BOOST_CHECK(m_processor->getLocalProof() != nullptr);
     BOOST_CHECK_EQUAL(m_processor->getLocalProof()->getId(),
                       localProof->getId());
     BOOST_CHECK_EQUAL(AvalancheTest::getMinQuorumScore(*m_processor), minScore);
     BOOST_CHECK_EQUAL(
         AvalancheTest::getMinQuorumConnectedScoreRatio(*m_processor), 0.5);
 
     // The local proof has not been validated yet
     m_processor->withPeerManager([&](avalanche::PeerManager &pm) {
         BOOST_CHECK_EQUAL(pm.getTotalPeersScore(), 0);
         BOOST_CHECK_EQUAL(pm.getConnectedPeersScore(), 0);
     });
     BOOST_CHECK(!m_processor->isQuorumEstablished());
 
     // Register the local proof. This is normally done when the chain tip is
     // updated. The local proof should be accounted for in the min quorum
     // computation but the peer manager doesn't know about that.
     m_processor->withPeerManager([&](avalanche::PeerManager &pm) {
         BOOST_CHECK(pm.registerProof(m_processor->getLocalProof()));
         BOOST_CHECK(pm.isBoundToPeer(m_processor->getLocalProof()->getId()));
         BOOST_CHECK_EQUAL(pm.getTotalPeersScore(), minScore / 4);
         BOOST_CHECK_EQUAL(pm.getConnectedPeersScore(), 0);
     });
     BOOST_CHECK(!m_processor->isQuorumEstablished());
 
     // Add enough nodes to get a conclusive vote
     for (NodeId id = 0; id < 8; id++) {
         m_processor->withPeerManager([&](avalanche::PeerManager &pm) {
             pm.addNode(id, m_processor->getLocalProof()->getId());
             BOOST_CHECK_EQUAL(pm.getTotalPeersScore(), minScore / 4);
             BOOST_CHECK_EQUAL(pm.getConnectedPeersScore(), minScore / 4);
         });
     }
 
     // Add part of the required stake and make sure we still report no quorum
     auto proof1 = buildRandomProof(active_chainstate, minScore / 2);
     m_processor->withPeerManager([&](avalanche::PeerManager &pm) {
         BOOST_CHECK(pm.registerProof(proof1));
         BOOST_CHECK_EQUAL(pm.getTotalPeersScore(), 3 * minScore / 4);
         BOOST_CHECK_EQUAL(pm.getConnectedPeersScore(), minScore / 4);
     });
     BOOST_CHECK(!m_processor->isQuorumEstablished());
 
     // Add the rest of the stake, but we are still lacking connected stake
     const int64_t tipTime =
         WITH_LOCK(chainman.GetMutex(), return chainman.ActiveTip())
             ->GetBlockTime();
     const COutPoint utxo{TxId(GetRandHash()), 0};
     const Amount amount = (int64_t(minScore / 4) * COIN) / 100;
     const int height = 100;
     const bool isCoinbase = false;
     {
         LOCK(cs_main);
         CCoinsViewCache &coins = active_chainstate.CoinsTip();
         coins.AddCoin(utxo,
                       Coin(CTxOut(amount, GetScriptForDestination(
                                               PKHash(key.GetPubKey()))),
                            height, isCoinbase),
                       false);
     }
     ProofBuilder pb(1, tipTime + 1, key, UNSPENDABLE_ECREG_PAYOUT_SCRIPT);
     BOOST_CHECK(pb.addUTXO(utxo, amount, height, isCoinbase, key));
     auto proof2 = pb.build();
 
     m_processor->withPeerManager([&](avalanche::PeerManager &pm) {
         BOOST_CHECK(pm.registerProof(proof2));
         BOOST_CHECK_EQUAL(pm.getTotalPeersScore(), minScore);
         BOOST_CHECK_EQUAL(pm.getConnectedPeersScore(), minScore / 4);
     });
     BOOST_CHECK(!m_processor->isQuorumEstablished());
 
     // Adding a node should cause the quorum to be detected and locked-in
     m_processor->withPeerManager([&](avalanche::PeerManager &pm) {
         pm.addNode(8, proof2->getId());
         BOOST_CHECK_EQUAL(pm.getTotalPeersScore(), minScore);
         // The peer manager knows that proof2 has a node attached ...
         BOOST_CHECK_EQUAL(pm.getConnectedPeersScore(), minScore / 2);
     });
     // ... but the processor also account for the local proof, so we reached 50%
     BOOST_CHECK(m_processor->isQuorumEstablished());
 
     // Go back to not having enough connected score, but we've already latched
     // the quorum as established
     m_processor->withPeerManager([&](avalanche::PeerManager &pm) {
         pm.removeNode(8);
         BOOST_CHECK_EQUAL(pm.getTotalPeersScore(), minScore);
         BOOST_CHECK_EQUAL(pm.getConnectedPeersScore(), minScore / 4);
     });
     BOOST_CHECK(m_processor->isQuorumEstablished());
 
     // Removing one more node drops our count below the minimum and the quorum
     // is no longer ready
     m_processor->withPeerManager(
         [&](avalanche::PeerManager &pm) { pm.removeNode(7); });
     BOOST_CHECK(!m_processor->isQuorumEstablished());
 
     // It resumes when we have enough nodes again
     m_processor->withPeerManager([&](avalanche::PeerManager &pm) {
         pm.addNode(7, m_processor->getLocalProof()->getId());
     });
     BOOST_CHECK(m_processor->isQuorumEstablished());
 
     // Remove peers one at a time until the quorum is no longer established
     auto spendProofUtxo = [&](ProofRef proof) {
         {
             LOCK(cs_main);
             CCoinsViewCache &coins = chainman.ActiveChainstate().CoinsTip();
             coins.SpendCoin(proof->getStakes()[0].getStake().getUTXO());
         }
         m_processor->withPeerManager([&proof](avalanche::PeerManager &pm) {
             pm.updatedBlockTip();
             BOOST_CHECK(!pm.isBoundToPeer(proof->getId()));
         });
     };
 
     // Expire proof2, the quorum is still latched
     for (int64_t i = 0; i < 6; i++) {
         SetMockTime(proof2->getExpirationTime() + i);
         CreateAndProcessBlock({}, CScript());
     }
     BOOST_CHECK_EQUAL(
         WITH_LOCK(chainman.GetMutex(), return chainman.ActiveTip())
             ->GetMedianTimePast(),
         proof2->getExpirationTime());
     m_processor->withPeerManager([&](avalanche::PeerManager &pm) {
         pm.updatedBlockTip();
         BOOST_CHECK(!pm.exists(proof2->getId()));
     });
     m_processor->withPeerManager([&](avalanche::PeerManager &pm) {
         BOOST_CHECK_EQUAL(pm.getTotalPeersScore(), 3 * minScore / 4);
         BOOST_CHECK_EQUAL(pm.getConnectedPeersScore(), minScore / 4);
     });
     BOOST_CHECK(m_processor->isQuorumEstablished());
 
     spendProofUtxo(proof1);
     m_processor->withPeerManager([&](avalanche::PeerManager &pm) {
         BOOST_CHECK_EQUAL(pm.getTotalPeersScore(), minScore / 4);
         BOOST_CHECK_EQUAL(pm.getConnectedPeersScore(), minScore / 4);
     });
     BOOST_CHECK(m_processor->isQuorumEstablished());
 
     spendProofUtxo(m_processor->getLocalProof());
     m_processor->withPeerManager([&](avalanche::PeerManager &pm) {
         BOOST_CHECK_EQUAL(pm.getTotalPeersScore(), 0);
         BOOST_CHECK_EQUAL(pm.getConnectedPeersScore(), 0);
     });
     // There is no node left
     BOOST_CHECK(!m_processor->isQuorumEstablished());
 }
 
 BOOST_AUTO_TEST_CASE(quorum_detection_parameter_validation) {
     // Create vector of tuples of:
     // <min stake, min ratio, min avaproofs messages, success bool>
     const std::vector<std::tuple<std::string, std::string, std::string, bool>>
         testCases = {
             // All parameters are invalid
             {"", "", "", false},
             {"-1", "-1", "-1", false},
 
             // Min stake is out of range
             {"-1", "0", "0", false},
             {"-0.01", "0", "0", false},
             {"21000000000000.01", "0", "0", false},
 
             // Min connected ratio is out of range
             {"0", "-1", "0", false},
             {"0", "1.1", "0", false},
 
             // Min avaproofs messages ratio is out of range
             {"0", "0", "-1", false},
 
             // All parameters are valid
             {"0", "0", "0", true},
             {"0.00", "0", "0", true},
             {"0.01", "0", "0", true},
             {"1", "0.1", "0", true},
             {"10", "0.5", "0", true},
             {"10", "1", "0", true},
             {"21000000000000.00", "0", "0", true},
             {"0", "0", "1", true},
             {"0", "0", "100", true},
         };
 
     // For each case set the parameters and check that making the processor
     // succeeds or fails as expected
     for (const auto &[stake, stakeRatio, numProofsMessages, success] :
          testCases) {
         setArg("-avaminquorumstake", stake);
         setArg("-avaminquorumconnectedstakeratio", stakeRatio);
         setArg("-avaminavaproofsnodecount", numProofsMessages);
 
         bilingual_str error;
         std::unique_ptr<Processor> processor = Processor::MakeProcessor(
             *m_node.args, *m_node.chain, m_node.connman.get(),
             *Assert(m_node.chainman), m_node.mempool.get(), *m_node.scheduler,
             error);
 
         if (success) {
             BOOST_CHECK(processor != nullptr);
             BOOST_CHECK(error.empty());
             BOOST_CHECK_EQUAL(error.original, "");
         } else {
             BOOST_CHECK(processor == nullptr);
             BOOST_CHECK(!error.empty());
             BOOST_CHECK(error.original != "");
         }
     }
 }
 
 BOOST_AUTO_TEST_CASE(min_avaproofs_messages) {
     ChainstateManager &chainman = *Assert(m_node.chainman);
 
     auto checkMinAvaproofsMessages = [&](int64_t minAvaproofsMessages) {
         setArg("-avaminavaproofsnodecount", ToString(minAvaproofsMessages));
 
         bilingual_str error;
         auto processor = Processor::MakeProcessor(
             *m_node.args, *m_node.chain, m_node.connman.get(), chainman,
             m_node.mempool.get(), *m_node.scheduler, error);
 
         auto addNode = [&](NodeId nodeid) {
             auto proof = buildRandomProof(chainman.ActiveChainstate(),
                                           MIN_VALID_PROOF_SCORE);
             processor->withPeerManager([&](avalanche::PeerManager &pm) {
                 BOOST_CHECK(pm.registerProof(proof));
                 BOOST_CHECK(pm.addNode(nodeid, proof->getId()));
             });
         };
 
         // Add enough node to have a conclusive vote, but don't account any
         // avaproofs.
         // NOTE: we can't use the test facilites like ConnectNodes() because we
         // are not testing on m_processor.
         for (NodeId id = 100; id < 108; id++) {
             addNode(id);
         }
 
         BOOST_CHECK_EQUAL(processor->isQuorumEstablished(),
                           minAvaproofsMessages <= 0);
 
         for (int64_t i = 0; i < minAvaproofsMessages - 1; i++) {
             addNode(i);
 
             processor->avaproofsSent(i);
             BOOST_CHECK_EQUAL(processor->getAvaproofsNodeCounter(), i + 1);
 
             // Receiving again on the same node does not increase the counter
             processor->avaproofsSent(i);
             BOOST_CHECK_EQUAL(processor->getAvaproofsNodeCounter(), i + 1);
 
             BOOST_CHECK(!processor->isQuorumEstablished());
         }
 
         addNode(minAvaproofsMessages);
         processor->avaproofsSent(minAvaproofsMessages);
         BOOST_CHECK(processor->isQuorumEstablished());
 
         // Check the latch
         AvalancheTest::clearavaproofsNodeCounter(*processor);
         BOOST_CHECK(processor->isQuorumEstablished());
     };
 
     checkMinAvaproofsMessages(0);
     checkMinAvaproofsMessages(1);
     checkMinAvaproofsMessages(10);
     checkMinAvaproofsMessages(100);
 }
 
 BOOST_AUTO_TEST_CASE_TEMPLATE(voting_parameters, P, VoteItemProviders) {
     // Check that setting voting parameters has the expected effect
     setArg("-avastalevotethreshold",
            ToString(AVALANCHE_VOTE_STALE_MIN_THRESHOLD));
     setArg("-avastalevotefactor", "2");
 
     const std::vector<std::tuple<int, int>> testCases = {
         // {number of yes votes, number of neutral votes}
         {0, AVALANCHE_VOTE_STALE_MIN_THRESHOLD},
         {AVALANCHE_FINALIZATION_SCORE + 4, AVALANCHE_FINALIZATION_SCORE - 6},
     };
 
     bilingual_str error;
     m_processor = Processor::MakeProcessor(
         *m_node.args, *m_node.chain, m_node.connman.get(),
         *Assert(m_node.chainman), m_node.mempool.get(), *m_node.scheduler,
         error);
 
     BOOST_CHECK(m_processor != nullptr);
     BOOST_CHECK(error.empty());
 
     P provider(this);
     const uint32_t invType = provider.invType;
 
     const auto item = provider.buildVoteItem();
     const auto itemid = provider.getVoteItemId(item);
 
     // Create nodes that supports avalanche.
     auto avanodes = ConnectNodes();
     int nextNodeIndex = 0;
 
     std::vector<avalanche::VoteItemUpdate> updates;
     for (const auto &[numYesVotes, numNeutralVotes] : testCases) {
         // Add a new item. Check it is added to the polls.
         BOOST_CHECK(addToReconcile(item));
         auto invs = getInvsForNextPoll();
         BOOST_CHECK_EQUAL(invs.size(), 1);
         BOOST_CHECK_EQUAL(invs[0].type, invType);
         BOOST_CHECK(invs[0].hash == itemid);
 
         BOOST_CHECK(m_processor->isAccepted(item));
 
         auto registerNewVote = [&](const Response &resp) {
             runEventLoop();
             auto nodeid = avanodes[nextNodeIndex++ % avanodes.size()]->GetId();
             BOOST_CHECK(registerVotes(nodeid, resp, updates));
         };
 
         // Add some confidence
         for (int i = 0; i < numYesVotes; i++) {
             Response resp = {getRound(), 0, {Vote(0, itemid)}};
             registerNewVote(next(resp));
             BOOST_CHECK(m_processor->isAccepted(item));
             BOOST_CHECK_EQUAL(m_processor->getConfidence(item),
                               i >= 6 ? i - 5 : 0);
             BOOST_CHECK_EQUAL(updates.size(), 0);
         }
 
         // Vote until just before item goes stale
         for (int i = 0; i < numNeutralVotes; i++) {
             Response resp = {getRound(), 0, {Vote(-1, itemid)}};
             registerNewVote(next(resp));
             BOOST_CHECK_EQUAL(updates.size(), 0);
         }
 
         // As long as it is not stale, we poll.
         invs = getInvsForNextPoll();
         BOOST_CHECK_EQUAL(invs.size(), 1);
         BOOST_CHECK_EQUAL(invs[0].type, invType);
         BOOST_CHECK(invs[0].hash == itemid);
 
         // Now stale
         Response resp = {getRound(), 0, {Vote(-1, itemid)}};
         registerNewVote(next(resp));
         BOOST_CHECK_EQUAL(updates.size(), 1);
         BOOST_CHECK(provider.fromAnyVoteItem(updates[0].getVoteItem()) == item);
         BOOST_CHECK(updates[0].getStatus() == VoteStatus::Stale);
         updates.clear();
 
         // Once stale, there is no poll for it.
         invs = getInvsForNextPoll();
         BOOST_CHECK_EQUAL(invs.size(), 0);
     }
 }
 
 BOOST_AUTO_TEST_CASE(block_vote_finalization_tip) {
     BlockProvider provider(this);
 
     BOOST_CHECK(!m_processor->hasFinalizedTip());
 
     std::vector<CBlockIndex *> blockIndexes;
     for (size_t i = 0; i < AVALANCHE_MAX_ELEMENT_POLL; i++) {
         CBlockIndex *pindex = provider.buildVoteItem();
         BOOST_CHECK(addToReconcile(pindex));
         blockIndexes.push_back(pindex);
     }
 
     auto invs = getInvsForNextPoll();
     BOOST_CHECK_EQUAL(invs.size(), AVALANCHE_MAX_ELEMENT_POLL);
     for (size_t i = 0; i < AVALANCHE_MAX_ELEMENT_POLL; i++) {
         BOOST_CHECK_EQUAL(
             invs[i].hash,
             blockIndexes[AVALANCHE_MAX_ELEMENT_POLL - i - 1]->GetBlockHash());
     }
 
     // Build a vote vector with the 11th block only being accepted and others
     // unknown.
     const BlockHash eleventhBlockHash =
         blockIndexes[AVALANCHE_MAX_ELEMENT_POLL - 10 - 1]->GetBlockHash();
     std::vector<Vote> votes;
     votes.reserve(AVALANCHE_MAX_ELEMENT_POLL);
     for (size_t i = AVALANCHE_MAX_ELEMENT_POLL; i > 0; i--) {
         BlockHash blockhash = blockIndexes[i - 1]->GetBlockHash();
         votes.emplace_back(blockhash == eleventhBlockHash ? 0 : -1, blockhash);
     }
 
     auto avanodes = ConnectNodes();
     int nextNodeIndex = 0;
 
     std::vector<avalanche::VoteItemUpdate> updates;
     auto registerNewVote = [&]() {
         Response resp = {getRound(), 0, votes};
         runEventLoop();
         auto nodeid = avanodes[nextNodeIndex++ % avanodes.size()]->GetId();
         BOOST_CHECK(registerVotes(nodeid, resp, updates));
     };
 
     BOOST_CHECK(!m_processor->hasFinalizedTip());
 
     // Vote for the blocks until the one being accepted finalizes
     bool eleventhBlockFinalized = false;
     for (size_t i = 0; i < 10000 && !eleventhBlockFinalized; i++) {
         registerNewVote();
 
         for (auto &update : updates) {
             if (update.getStatus() == VoteStatus::Finalized &&
                 provider.fromAnyVoteItem(update.getVoteItem())
                         ->GetBlockHash() == eleventhBlockHash) {
                 eleventhBlockFinalized = true;
                 BOOST_CHECK(m_processor->hasFinalizedTip());
             } else {
                 BOOST_CHECK(!m_processor->hasFinalizedTip());
             }
         }
     }
     BOOST_CHECK(eleventhBlockFinalized);
     BOOST_CHECK(m_processor->hasFinalizedTip());
 
     // From now only the 10 blocks with more work are polled for
     invs = getInvsForNextPoll();
     BOOST_CHECK_EQUAL(invs.size(), 10);
     for (size_t i = 0; i < 10; i++) {
         BOOST_CHECK_EQUAL(
             invs[i].hash,
             blockIndexes[AVALANCHE_MAX_ELEMENT_POLL - i - 1]->GetBlockHash());
     }
 
     // Adding ancestor blocks to reconcile will fail
     for (size_t i = 0; i < AVALANCHE_MAX_ELEMENT_POLL - 10 - 1; i++) {
         BOOST_CHECK(!addToReconcile(blockIndexes[i]));
     }
 
     // Create a couple concurrent chain tips
     CBlockIndex *tip = provider.buildVoteItem();
 
     auto &activeChainstate = m_node.chainman->ActiveChainstate();
     BlockValidationState state;
     activeChainstate.InvalidateBlock(state, tip);
 
     // Use another script to make sure we don't generate the same block again
     CBlock altblock = CreateAndProcessBlock({}, CScript() << OP_TRUE);
     auto alttip = WITH_LOCK(
         cs_main, return Assert(m_node.chainman)
                      ->m_blockman.LookupBlockIndex(altblock.GetHash()));
     BOOST_CHECK(alttip);
     BOOST_CHECK(alttip->pprev == tip->pprev);
     BOOST_CHECK(alttip->GetBlockHash() != tip->GetBlockHash());
 
     // Reconsider the previous tip valid, so we have concurrent tip candidates
     {
         LOCK(cs_main);
         activeChainstate.ResetBlockFailureFlags(tip);
     }
     activeChainstate.ActivateBestChain(state);
 
     BOOST_CHECK(addToReconcile(tip));
     BOOST_CHECK(addToReconcile(alttip));
     invs = getInvsForNextPoll();
     BOOST_CHECK_EQUAL(invs.size(), 12);
 
     // Vote for the tip until it finalizes
     BlockHash tiphash = tip->GetBlockHash();
     votes.clear();
     votes.reserve(12);
     for (auto &inv : invs) {
         votes.emplace_back(inv.hash == tiphash ? 0 : -1, inv.hash);
     }
 
     bool tipFinalized = false;
     for (size_t i = 0; i < 10000 && !tipFinalized; i++) {
         registerNewVote();
 
         for (auto &update : updates) {
             if (update.getStatus() == VoteStatus::Finalized &&
                 provider.fromAnyVoteItem(update.getVoteItem())
                         ->GetBlockHash() == tiphash) {
                 tipFinalized = true;
             }
         }
     }
     BOOST_CHECK(tipFinalized);
 
     // Now the tip and all its ancestors will be removed from polls. Only the
     // alttip remains because it is on a forked chain so we want to keep polling
     // for that one until it's invalidated or stalled.
     invs = getInvsForNextPoll();
     BOOST_CHECK_EQUAL(invs.size(), 1);
     BOOST_CHECK_EQUAL(invs[0].hash, alttip->GetBlockHash());
 
     // Cannot reconcile a finalized block
     BOOST_CHECK(!addToReconcile(tip));
 
     // Vote for alttip until it invalidates
     BlockHash alttiphash = alttip->GetBlockHash();
     votes = {{1, alttiphash}};
 
     bool alttipInvalidated = false;
     for (size_t i = 0; i < 10000 && !alttipInvalidated; i++) {
         registerNewVote();
 
         for (auto &update : updates) {
             if (update.getStatus() == VoteStatus::Invalid &&
                 provider.fromAnyVoteItem(update.getVoteItem())
                         ->GetBlockHash() == alttiphash) {
                 alttipInvalidated = true;
             }
         }
     }
     BOOST_CHECK(alttipInvalidated);
     invs = getInvsForNextPoll();
     BOOST_CHECK_EQUAL(invs.size(), 0);
 
     // Cannot reconcile an invalidated block
     BOOST_CHECK(!addToReconcile(alttip));
 }
 
 BOOST_AUTO_TEST_CASE(vote_map_comparator) {
     ChainstateManager &chainman = *Assert(m_node.chainman);
     Chainstate &activeChainState = chainman.ActiveChainstate();
 
     const int numberElementsEachType = 100;
     FastRandomContext rng;
 
     std::vector<ProofRef> proofs;
     for (size_t i = 1; i <= numberElementsEachType; i++) {
         auto proof =
             buildRandomProof(activeChainState, i * MIN_VALID_PROOF_SCORE);
         BOOST_CHECK(proof != nullptr);
         proofs.emplace_back(std::move(proof));
     }
     Shuffle(proofs.begin(), proofs.end(), rng);
 
     std::vector<CBlockIndex> indexes;
     for (size_t i = 1; i <= numberElementsEachType; i++) {
         CBlockIndex index;
         index.nChainWork = i;
         indexes.emplace_back(std::move(index));
     }
     Shuffle(indexes.begin(), indexes.end(), rng);
 
     auto allItems = std::make_tuple(std::move(proofs), std::move(indexes));
     static const size_t numTypes = std::tuple_size<decltype(allItems)>::value;
 
     RWCollection<VoteMap> voteMap;
 
     {
         auto writeView = voteMap.getWriteView();
         for (size_t i = 0; i < numberElementsEachType; i++) {
             // Randomize the insert order at each loop increment
             const size_t firstType = rng.randrange(numTypes);
 
             for (size_t j = 0; j < numTypes; j++) {
                 switch ((firstType + j) % numTypes) {
                     // ProofRef
                     case 0:
                         writeView->insert(std::make_pair(
                             std::get<0>(allItems)[i], VoteRecord(true)));
                         break;
                     // CBlockIndex *
                     case 1:
                         writeView->insert(std::make_pair(
                             &std::get<1>(allItems)[i], VoteRecord(true)));
                         break;
                     default:
                         break;
                 }
             }
         }
     }
 
     {
         // Check ordering
         auto readView = voteMap.getReadView();
         auto it = readView.begin();
 
         // The first batch of items is the proofs ordered by score (descending)
         uint32_t lastScore = std::numeric_limits<uint32_t>::max();
         for (size_t i = 0; i < numberElementsEachType; i++) {
             BOOST_CHECK(std::holds_alternative<const ProofRef>(it->first));
 
             uint32_t currentScore =
                 std::get<const ProofRef>(it->first)->getScore();
             BOOST_CHECK_LT(currentScore, lastScore);
             lastScore = currentScore;
 
             it++;
         }
 
         // The next batch of items is the block indexes ordered by work
         // (descending)
         arith_uint256 lastWork = ~arith_uint256(0);
         for (size_t i = 0; i < numberElementsEachType; i++) {
             BOOST_CHECK(std::holds_alternative<const CBlockIndex *>(it->first));
 
             arith_uint256 currentWork =
                 std::get<const CBlockIndex *>(it->first)->nChainWork;
             BOOST_CHECK(currentWork < lastWork);
             lastWork = currentWork;
 
             it++;
         }
 
         BOOST_CHECK(it == readView.end());
     }
 }
 
 BOOST_AUTO_TEST_CASE(block_reconcile_initial_vote) {
     auto &chainman = Assert(m_node.chainman);
     Chainstate &chainstate = chainman->ActiveChainstate();
 
     const auto block = std::make_shared<const CBlock>(
         this->CreateBlock({}, CScript(), chainstate));
     const BlockHash blockhash = block->GetHash();
 
     BlockValidationState state;
     CBlockIndex *blockindex;
     {
         LOCK(cs_main);
         BOOST_CHECK(chainstate.AcceptBlock(block, state,
                                            /*fRequested=*/true, /*dbp=*/nullptr,
                                            /*fNewBlock=*/nullptr,
                                            /*min_pow_checked=*/true));
 
         blockindex = chainman->m_blockman.LookupBlockIndex(blockhash);
         BOOST_CHECK(blockindex);
     }
 
     // The block is not connected yet, and not added to the poll list yet
     BOOST_CHECK(AvalancheTest::getInvsForNextPoll(*m_processor).empty());
     BOOST_CHECK(!m_processor->isAccepted(blockindex));
 
     // Call ActivateBestChain to connect the new block
     BOOST_CHECK(chainstate.ActivateBestChain(state, block, m_processor.get()));
     // It is a valid block so the tip is updated
     BOOST_CHECK_EQUAL(chainstate.m_chain.Tip(), blockindex);
 
     // Check the block is added to the poll
     auto invs = AvalancheTest::getInvsForNextPoll(*m_processor);
     BOOST_CHECK_EQUAL(invs.size(), 1);
     BOOST_CHECK_EQUAL(invs[0].type, MSG_BLOCK);
     BOOST_CHECK_EQUAL(invs[0].hash, blockhash);
 
     // This block is our new tip so we should vote "yes"
     BOOST_CHECK(m_processor->isAccepted(blockindex));
 
     // Prevent a data race between UpdatedBlockTip and the Processor destructor
     SyncWithValidationInterfaceQueue();
 }
 
 BOOST_AUTO_TEST_CASE(compute_staking_rewards) {
     auto now = GetTime<std::chrono::seconds>();
     SetMockTime(now);
 
     // Pick in the middle
     BlockHash prevBlockHash{uint256::ZERO};
 
     std::vector<CScript> winners;
 
     BOOST_CHECK(!m_processor->getStakingRewardWinners(prevBlockHash, winners));
 
     // Null index
     BOOST_CHECK(!m_processor->computeStakingReward(nullptr));
     BOOST_CHECK(!m_processor->getStakingRewardWinners(prevBlockHash, winners));
 
     CBlockIndex prevBlock;
     prevBlock.phashBlock = &prevBlockHash;
     prevBlock.nHeight = 100;
     prevBlock.nTime = now.count();
 
     // No quorum
     BOOST_CHECK(!m_processor->computeStakingReward(&prevBlock));
     BOOST_CHECK(!m_processor->getStakingRewardWinners(prevBlockHash, winners));
 
     // Setup a bunch of proofs
     size_t numProofs = 10;
     std::vector<ProofRef> proofs;
     proofs.reserve(numProofs);
     for (size_t i = 0; i < numProofs; i++) {
         const CKey key = CKey::MakeCompressedKey();
         CScript payoutScript = GetScriptForRawPubKey(key.GetPubKey());
 
         auto proof = GetProof(payoutScript);
         m_processor->withPeerManager([&](avalanche::PeerManager &pm) {
             BOOST_CHECK(pm.registerProof(proof));
             BOOST_CHECK(pm.addNode(i, proof->getId()));
             // Finalize the proof
             BOOST_CHECK(pm.forPeer(proof->getId(), [&](const Peer peer) {
                 return pm.setFinalized(peer.peerid);
             }));
         });
 
         proofs.emplace_back(std::move(proof));
     }
 
     BOOST_CHECK(m_processor->isQuorumEstablished());
 
     // Proofs are too recent so we still have no winner
     BOOST_CHECK(!m_processor->computeStakingReward(&prevBlock));
     BOOST_CHECK(!m_processor->getStakingRewardWinners(prevBlockHash, winners));
 
     // Make sure we picked a payout script from one of our proofs
     auto winnerExists = [&](const CScript &expectedWinner) {
         const std::string winnerString = FormatScript(expectedWinner);
 
         for (const ProofRef &proof : proofs) {
             if (winnerString == FormatScript(proof->getPayoutScript())) {
                 return true;
             }
         }
         return false;
     };
 
     // Elapse some time
     now += 1h + 1s;
     SetMockTime(now);
     prevBlock.nTime = now.count();
 
     // Now we successfully inserted a winner in our map
     BOOST_CHECK(m_processor->computeStakingReward(&prevBlock));
     BOOST_CHECK(m_processor->getStakingRewardWinners(prevBlockHash, winners));
     BOOST_CHECK(winnerExists(winners[0]));
 
     // Subsequent calls are a no-op
     BOOST_CHECK(m_processor->computeStakingReward(&prevBlock));
     BOOST_CHECK(m_processor->getStakingRewardWinners(prevBlockHash, winners));
     BOOST_CHECK(winnerExists(winners[0]));
 
     CBlockIndex prevBlockHigh = prevBlock;
     BlockHash prevBlockHashHigh =
         BlockHash(ArithToUint256({std::numeric_limits<uint64_t>::max()}));
     prevBlockHigh.phashBlock = &prevBlockHashHigh;
     prevBlockHigh.nHeight = 101;
     BOOST_CHECK(m_processor->computeStakingReward(&prevBlockHigh));
     BOOST_CHECK(
         m_processor->getStakingRewardWinners(prevBlockHashHigh, winners));
     BOOST_CHECK(winnerExists(winners[0]));
 
     // No impact on previous winner so far
     BOOST_CHECK(m_processor->getStakingRewardWinners(prevBlockHash, winners));
     BOOST_CHECK(winnerExists(winners[0]));
 
     // Cleanup to height 101
     m_processor->cleanupStakingRewards(101);
 
     // Now the previous winner has been cleared
     BOOST_CHECK(!m_processor->getStakingRewardWinners(prevBlockHash, winners));
 
     // But the last one remain
     BOOST_CHECK(
         m_processor->getStakingRewardWinners(prevBlockHashHigh, winners));
     BOOST_CHECK(winnerExists(winners[0]));
 
     // We can add it again
     BOOST_CHECK(m_processor->computeStakingReward(&prevBlock));
     BOOST_CHECK(m_processor->getStakingRewardWinners(prevBlockHash, winners));
     BOOST_CHECK(winnerExists(winners[0]));
 
     // Cleanup to higher height
     m_processor->cleanupStakingRewards(200);
 
     // No winner anymore
     BOOST_CHECK(!m_processor->getStakingRewardWinners(prevBlockHash, winners));
     BOOST_CHECK(
         !m_processor->getStakingRewardWinners(prevBlockHashHigh, winners));
 }
 
 BOOST_AUTO_TEST_CASE(local_proof_status) {
     const CKey key = CKey::MakeCompressedKey();
 
     const COutPoint outpoint{TxId(GetRandHash()), 0};
     {
         CScript script = GetScriptForDestination(PKHash(key.GetPubKey()));
 
         LOCK(cs_main);
         CCoinsViewCache &coins =
             Assert(m_node.chainman)->ActiveChainstate().CoinsTip();
         coins.AddCoin(outpoint,
                       Coin(CTxOut(PROOF_DUST_THRESHOLD, script), 100, false),
                       false);
     }
 
     auto buildProof = [&](const COutPoint &outpoint, uint64_t sequence,
                           uint32_t height) {
         ProofBuilder pb(sequence, 0, key, UNSPENDABLE_ECREG_PAYOUT_SCRIPT);
         BOOST_CHECK(
             pb.addUTXO(outpoint, PROOF_DUST_THRESHOLD, height, false, key));
         return pb.build();
     };
 
     auto localProof = buildProof(outpoint, 1, 100);
 
     setArg("-avamasterkey", EncodeSecret(key));
     setArg("-avaproof", localProof->ToHex());
     setArg("-avalancheconflictingproofcooldown", "0");
     setArg("-avalanchepeerreplacementcooldown", "0");
     setArg("-avaproofstakeutxoconfirmations", "3");
 
     bilingual_str error;
     ChainstateManager &chainman = *Assert(m_node.chainman);
     m_processor = Processor::MakeProcessor(
         *m_node.args, *m_node.chain, m_node.connman.get(), chainman,
         m_node.mempool.get(), *m_node.scheduler, error);
 
     BOOST_CHECK_EQUAL(m_processor->getLocalProof()->getId(),
                       localProof->getId());
 
     auto checkLocalProofState =
         [&](const bool boundToPeer,
             const ProofRegistrationResult expectedResult) {
             BOOST_CHECK_EQUAL(
                 m_processor->withPeerManager([&](avalanche::PeerManager &pm) {
                     return pm.isBoundToPeer(localProof->getId());
                 }),
                 boundToPeer);
             BOOST_CHECK_MESSAGE(
                 m_processor->getLocalProofRegistrationState().GetResult() ==
                     expectedResult,
                 m_processor->getLocalProofRegistrationState().ToString());
         };
 
     checkLocalProofState(false, ProofRegistrationResult::NONE);
 
     // Not ready to share, the local proof isn't registered
     BOOST_CHECK(!m_processor->canShareLocalProof());
     AvalancheTest::updatedBlockTip(*m_processor);
     checkLocalProofState(false, ProofRegistrationResult::NONE);
 
     // Ready to share, but the proof is immature
     AvalancheTest::setLocalProofShareable(*m_processor, true);
     BOOST_CHECK(m_processor->canShareLocalProof());
     AvalancheTest::updatedBlockTip(*m_processor);
     checkLocalProofState(false, ProofRegistrationResult::IMMATURE);
 
     // Mine a block to re-evaluate the proof, it remains immature
     mineBlocks(1);
     AvalancheTest::updatedBlockTip(*m_processor);
     checkLocalProofState(false, ProofRegistrationResult::IMMATURE);
 
     // One more block and the proof turns mature
     mineBlocks(1);
     AvalancheTest::updatedBlockTip(*m_processor);
     checkLocalProofState(true, ProofRegistrationResult::NONE);
 
     // Build a conflicting proof and check the status is updated accordingly
     auto conflictingProof = buildProof(outpoint, 2, 100);
     m_processor->withPeerManager([&](avalanche::PeerManager &pm) {
         BOOST_CHECK(pm.registerProof(conflictingProof));
         BOOST_CHECK(pm.isBoundToPeer(conflictingProof->getId()));
         BOOST_CHECK(pm.isInConflictingPool(localProof->getId()));
     });
     AvalancheTest::updatedBlockTip(*m_processor);
     checkLocalProofState(false, ProofRegistrationResult::CONFLICTING);
 }
 
 BOOST_AUTO_TEST_CASE(reconcileOrFinalize) {
     setArg("-avalancheconflictingproofcooldown", "0");
     setArg("-avalanchepeerreplacementcooldown", "0");
 
     // Proof is null
     BOOST_CHECK(!m_processor->reconcileOrFinalize(ProofRef()));
 
     ChainstateManager &chainman = *Assert(m_node.chainman);
     Chainstate &activeChainState = chainman.ActiveChainstate();
 
     const CKey key = CKey::MakeCompressedKey();
     const COutPoint outpoint{TxId(GetRandHash()), 0};
     {
         CScript script = GetScriptForDestination(PKHash(key.GetPubKey()));
 
         LOCK(cs_main);
         CCoinsViewCache &coins = activeChainState.CoinsTip();
         coins.AddCoin(outpoint,
                       Coin(CTxOut(PROOF_DUST_THRESHOLD, script), 100, false),
                       false);
     }
 
     auto buildProof = [&](const COutPoint &outpoint, uint64_t sequence) {
         ProofBuilder pb(sequence, 0, key, UNSPENDABLE_ECREG_PAYOUT_SCRIPT);
         BOOST_CHECK(
             pb.addUTXO(outpoint, PROOF_DUST_THRESHOLD, 100, false, key));
         return pb.build();
     };
 
     auto proof = buildProof(outpoint, 1);
     BOOST_CHECK(proof);
 
     // Not a peer nor conflicting
     BOOST_CHECK(!m_processor->reconcileOrFinalize(proof));
 
     // Register the proof
     m_processor->withPeerManager([&](avalanche::PeerManager &pm) {
         BOOST_CHECK(pm.registerProof(proof));
         BOOST_CHECK(pm.isBoundToPeer(proof->getId()));
         BOOST_CHECK(!pm.isInConflictingPool(proof->getId()));
     });
 
     // Reconcile works
     BOOST_CHECK(m_processor->reconcileOrFinalize(proof));
     // Repeated calls fail and do nothing
     BOOST_CHECK(!m_processor->reconcileOrFinalize(proof));
 
     // Finalize
     AvalancheTest::addProofToRecentfinalized(*m_processor, proof->getId());
     BOOST_CHECK(m_processor->isRecentlyFinalized(proof->getId()));
     BOOST_CHECK(m_processor->reconcileOrFinalize(proof));
 
     m_processor->withPeerManager([&](avalanche::PeerManager &pm) {
         // The peer is marked as final
         BOOST_CHECK(pm.forPeer(proof->getId(), [&](const Peer &peer) {
             return peer.hasFinalized;
         }));
         BOOST_CHECK(pm.isBoundToPeer(proof->getId()));
         BOOST_CHECK(!pm.isInConflictingPool(proof->getId()));
     });
 
     // Same proof with a higher sequence number
     auto betterProof = buildProof(outpoint, 2);
     BOOST_CHECK(betterProof);
 
     // Not registered nor conflicting yet
     BOOST_CHECK(!m_processor->reconcileOrFinalize(betterProof));
 
     m_processor->withPeerManager([&](avalanche::PeerManager &pm) {
         BOOST_CHECK(pm.registerProof(betterProof));
         BOOST_CHECK(pm.isBoundToPeer(betterProof->getId()));
         BOOST_CHECK(!pm.isInConflictingPool(betterProof->getId()));
 
         BOOST_CHECK(!pm.isBoundToPeer(proof->getId()));
         BOOST_CHECK(pm.isInConflictingPool(proof->getId()));
     });
 
     // Recently finalized, not worth polling
     BOOST_CHECK(!m_processor->reconcileOrFinalize(proof));
     // But the better proof can be polled
     BOOST_CHECK(m_processor->reconcileOrFinalize(betterProof));
 }
 
 BOOST_AUTO_TEST_CASE(stake_contenders) {
     setArg("-avalanchestakingpreconsensus", "1");
     bilingual_str error;
     m_processor = Processor::MakeProcessor(
         *m_node.args, *m_node.chain, m_node.connman.get(),
         *Assert(m_node.chainman), m_node.mempool.get(), *m_node.scheduler,
         error);
     BOOST_CHECK(m_processor);
 
     auto now = GetTime<std::chrono::seconds>();
     SetMockTime(now);
 
     ChainstateManager &chainman = *Assert(m_node.chainman);
     Chainstate &active_chainstate = chainman.ActiveChainstate();
     CBlockIndex *chaintip =
         WITH_LOCK(chainman.GetMutex(), return chainman.ActiveTip());
 
     auto proof1 = buildRandomProof(active_chainstate, MIN_VALID_PROOF_SCORE);
     const ProofId proofid1 = proof1->getId();
     const StakeContenderId contender1_block1(chaintip->GetBlockHash(),
                                              proofid1);
 
     auto proof2 = buildRandomProof(active_chainstate, MIN_VALID_PROOF_SCORE);
     const ProofId proofid2 = proof2->getId();
     const StakeContenderId contender2_block1(chaintip->GetBlockHash(),
                                              proofid2);
 
     // Add stake contenders. Without computing staking rewards, the status is
     // pending.
     {
         LOCK(cs_main);
         m_processor->addStakeContender(proof1);
         m_processor->addStakeContender(proof2);
     }
     BOOST_CHECK_EQUAL(m_processor->getStakeContenderStatus(contender1_block1),
                       -2);
     BOOST_CHECK_EQUAL(m_processor->getStakeContenderStatus(contender2_block1),
                       -2);
 
     // Sanity check unknown contender
     const StakeContenderId unknownContender(chaintip->GetBlockHash(),
                                             ProofId(GetRandHash()));
     BOOST_CHECK_EQUAL(m_processor->getStakeContenderStatus(unknownContender),
                       -1);
 
     // Register proof2 and save it as a remote proof so that it will be promoted
     m_processor->withPeerManager([&](avalanche::PeerManager &pm) {
         ConnectNode(NODE_AVALANCHE);
         pm.registerProof(proof2);
         for (NodeId n = 0; n < 8; n++) {
             pm.addNode(n, proofid2);
         }
         pm.saveRemoteProof(proofid2, 0, true);
         BOOST_CHECK(pm.forPeer(proofid2, [&](const Peer peer) {
             return pm.setFinalized(peer.peerid);
         }));
     });
 
     // Need to have finalization tip set for contenders to be promoted
     AvalancheTest::setFinalizationTip(*m_processor, chaintip);
 
     // Make proofs old enough to be considered for staking rewards
     now += 1h + 1s;
     SetMockTime(now);
 
     // Advance chaintip
     CBlock block = CreateAndProcessBlock({}, CScript());
     chaintip =
         WITH_LOCK(cs_main, return Assert(m_node.chainman)
                                ->m_blockman.LookupBlockIndex(block.GetHash()));
     AvalancheTest::updatedBlockTip(*m_processor);
 
     // Compute local stake winner
     BOOST_CHECK(m_processor->isQuorumEstablished());
     BOOST_CHECK(m_processor->computeStakingReward(chaintip));
     {
         std::vector<CScript> winners;
         BOOST_CHECK(m_processor->getStakingRewardWinners(
             chaintip->GetBlockHash(), winners));
         BOOST_CHECK_EQUAL(winners.size(), 1);
         BOOST_CHECK(winners[0] == proof2->getPayoutScript());
     }
 
     // Sanity check unknown contender
     BOOST_CHECK_EQUAL(m_processor->getStakeContenderStatus(unknownContender),
                       -1);
 
     // Old contender cache entries unaffected
     BOOST_CHECK_EQUAL(m_processor->getStakeContenderStatus(contender1_block1),
                       -2);
     BOOST_CHECK_EQUAL(m_processor->getStakeContenderStatus(contender2_block1),
                       -2);
 
     // contender1 was not promoted
     const StakeContenderId contender1_block2 =
         StakeContenderId(chaintip->GetBlockHash(), proofid1);
     BOOST_CHECK_EQUAL(m_processor->getStakeContenderStatus(contender1_block2),
                       -1);
 
     // contender2 was promoted
-    // TODO contender2 is REJECTED because the locally selected stake winner is
-    // not yet factored into stake contenders. This will be ACCEPTED once it
-    // does.
     const StakeContenderId contender2_block2 =
         StakeContenderId(chaintip->GetBlockHash(), proofid2);
     BOOST_CHECK_EQUAL(m_processor->getStakeContenderStatus(contender2_block2),
-                      1);
+                      0);
 
     // Advance the finalization tip
     AvalancheTest::setFinalizationTip(*m_processor, chaintip);
 
     // Now that the finalization point has passed the block where contender1 was
     // added, cleaning up the cache will remove its entry. contender2 will have
     // its old entry cleaned up, but the promoted one remains.
     m_processor->cleanupStakingRewards(chaintip->nHeight);
 
     BOOST_CHECK_EQUAL(m_processor->getStakeContenderStatus(unknownContender),
                       -1);
 
     BOOST_CHECK_EQUAL(m_processor->getStakeContenderStatus(contender1_block1),
                       -1);
     BOOST_CHECK_EQUAL(m_processor->getStakeContenderStatus(contender1_block2),
                       -1);
 
     BOOST_CHECK_EQUAL(m_processor->getStakeContenderStatus(contender2_block1),
                       -1);
     BOOST_CHECK_EQUAL(m_processor->getStakeContenderStatus(contender2_block2),
-                      1);
+                      0);
 
     // Manually set contenders as winners
     m_processor->setStakingRewardWinners(
         chaintip, {proof1->getPayoutScript(), proof2->getPayoutScript()});
     // contender1 has been forgotten, which is expected. When a proof becomes
     // invalid and is cleaned up from the cache, we do not expect peers to poll
     // for it any more.
     BOOST_CHECK_EQUAL(m_processor->getStakeContenderStatus(contender1_block2),
                       -1);
     // contender2 is a winner despite avalanche not finalizing it
     BOOST_CHECK_EQUAL(m_processor->getStakeContenderStatus(contender2_block2),
                       0);
 
     // Reject proof2, mine a new chain tip, finalize it, and cleanup the cache
     m_processor->withPeerManager(
         [&](avalanche::PeerManager &pm) { pm.rejectProof(proofid2); });
     block = CreateAndProcessBlock({}, CScript());
     chaintip =
         WITH_LOCK(cs_main, return Assert(m_node.chainman)
                                ->m_blockman.LookupBlockIndex(block.GetHash()));
     AvalancheTest::updatedBlockTip(*m_processor);
     AvalancheTest::setFinalizationTip(*m_processor, chaintip);
     m_processor->cleanupStakingRewards(chaintip->nHeight);
 
     BOOST_CHECK_EQUAL(m_processor->getStakeContenderStatus(unknownContender),
                       -1);
 
     // Old entries were cleaned up
     BOOST_CHECK_EQUAL(m_processor->getStakeContenderStatus(contender1_block2),
                       -1);
     BOOST_CHECK_EQUAL(m_processor->getStakeContenderStatus(contender2_block2),
                       -1);
 
     // Neither contender was promoted and contender2 was cleaned up even though
     // it was once a manual winner.
     const StakeContenderId contender1_block3 =
         StakeContenderId(chaintip->GetBlockHash(), proofid1);
     BOOST_CHECK_EQUAL(m_processor->getStakeContenderStatus(contender1_block3),
                       -1);
     const StakeContenderId contender2_block3 =
         StakeContenderId(chaintip->GetBlockHash(), proofid2);
     BOOST_CHECK_EQUAL(m_processor->getStakeContenderStatus(contender2_block3),
                       -1);
 }
 
 BOOST_AUTO_TEST_SUITE_END()
diff --git a/test/functional/abc_p2p_avalanche_contender_voting.py b/test/functional/abc_p2p_avalanche_contender_voting.py
index 01cc29c53..ea8c025e9 100644
--- a/test/functional/abc_p2p_avalanche_contender_voting.py
+++ b/test/functional/abc_p2p_avalanche_contender_voting.py
@@ -1,183 +1,185 @@
 # Copyright (c) 2024 The Bitcoin developers
 # Distributed under the MIT software license, see the accompanying
 # file COPYING or http://www.opensource.org/licenses/mit-license.php.
 """Test the resolution of stake contender preconsensus via avalanche."""
 import time
 
 from test_framework.avatools import can_find_inv_in_poll, get_ava_p2p_interface
 from test_framework.key import ECPubKey
 from test_framework.messages import (
     MSG_AVA_STAKE_CONTENDER,
     AvalancheContenderVoteError,
     AvalancheVote,
     hash256,
     ser_uint256,
 )
 from test_framework.test_framework import BitcoinTestFramework
 from test_framework.util import assert_equal, uint256_hex
 
 QUORUM_NODE_COUNT = 16
 
 
 class AvalancheContenderVotingTest(BitcoinTestFramework):
     def set_test_params(self):
         self.setup_clean_chain = True
         self.num_nodes = 1
         self.extra_args = [
             [
                 "-avalanchestakingpreconsensus=1",
                 "-avalanchestakingrewards=1",
                 "-avaproofstakeutxodustthreshold=1000000",
                 "-avaproofstakeutxoconfirmations=1",
                 "-avacooldown=0",
                 "-avaminquorumstake=0",
                 "-avaminavaproofsnodecount=0",
                 "-whitelist=noban@127.0.0.1",
                 "-persistavapeers=0",
             ],
         ]
         self.supports_cli = False
 
     def run_test(self):
         node = self.nodes[0]
 
         # Set mock time so we can control when proofs will be considered for staking rewards
         now = int(time.time())
         node.setmocktime(now)
 
         # Build a fake quorum of nodes.
         def get_quorum():
             def new_ava_interface(node):
                 # This test depends on each proof being added to the contender cache before
                 # the next block arrives, so we wait until that happens.
                 peer = get_ava_p2p_interface(self, node)
                 blockhash = node.getbestblockhash()
                 self.wait_until(
                     lambda: node.getstakecontendervote(
                         blockhash, uint256_hex(peer.proof.proofid)
                     )
                     == AvalancheContenderVoteError.PENDING
                 )
                 return peer
 
             return [new_ava_interface(node) for _ in range(0, QUORUM_NODE_COUNT)]
 
         # Pick one node from the quorum for polling.
         quorum = get_quorum()
         poll_node = quorum[0]
 
         assert node.getavalancheinfo()["ready_to_poll"] is True
 
         def has_finalized_proof(proofid):
             can_find_inv_in_poll(quorum, proofid)
             return node.getrawavalancheproof(uint256_hex(proofid))["finalized"]
 
         for peer in quorum:
             self.wait_until(lambda: has_finalized_proof(peer.proof.proofid))
 
         # Get the key so we can verify signatures.
         avakey = ECPubKey()
         avakey.set(bytes.fromhex(node.getavalanchekey()))
 
         def assert_response(expected):
             response = poll_node.wait_for_avaresponse()
             r = response.response
 
             # Verify signature
             assert avakey.verify_schnorr(response.sig, r.get_hash())
 
             votes = r.votes
             assert_equal(len(votes), len(expected))
             for i in range(0, len(votes)):
                 assert_equal(repr(votes[i]), repr(expected[i]))
 
         def make_contender_id(prevblockhash, proofid):
             return int.from_bytes(
                 hash256(ser_uint256(int(prevblockhash, 16)) + ser_uint256(proofid)),
                 "little",
             )
 
         # Before finalizing any blocks, no contender promotion occurs in the cache,
         # so the only way to test if the node knows about a particular contender is
         # to check it at the block height that the proof was first seen at.
         for i in range(0, QUORUM_NODE_COUNT):
             # We started with a clean chain and a new block is mined when creating
             # each quorum proof to make it valid, so the first block the proof was
             # seen at is the quorum proof's index + 1.
             blockhash = node.getblockhash(i + 1)
             poll_ids = []
             expected = []
             for p in range(0, QUORUM_NODE_COUNT):
                 contender_proof = quorum[p].proof
                 contender_id = make_contender_id(blockhash, contender_proof.proofid)
                 poll_ids.append(contender_id)
                 # If the node knows about the contender, it will respond as INVALID
                 expected_vote = (
                     AvalancheContenderVoteError.PENDING
                     if p == i
                     else AvalancheContenderVoteError.UNKNOWN
                 )
                 expected.append(AvalancheVote(expected_vote, contender_id))
             poll_node.send_poll(poll_ids, inv_type=MSG_AVA_STAKE_CONTENDER)
             assert_response(expected)
 
         # Unknown contender
         unknown_contender_id = 0x123
         poll_node.send_poll([unknown_contender_id], inv_type=MSG_AVA_STAKE_CONTENDER)
         assert_response(
             [AvalancheVote(AvalancheContenderVoteError.UNKNOWN, unknown_contender_id)]
         )
 
         def has_finalized_tip(tip_expected):
             hash_tip_final = int(tip_expected, 16)
             can_find_inv_in_poll(quorum, hash_tip_final)
             return node.isfinalblock(tip_expected)
 
         # Finalize a block so we promote the contender cache with every block
         tip = node.getbestblockhash()
         self.wait_until(lambda: has_finalized_tip(tip))
         assert_equal(node.getbestblockhash(), tip)
 
         # Now trigger building the whole cache for a block
         tip = self.generate(node, 1)[0]
 
         # Unknown contender is still unknown
         poll_node.send_poll([unknown_contender_id], inv_type=MSG_AVA_STAKE_CONTENDER)
         assert_response(
             [AvalancheVote(AvalancheContenderVoteError.UNKNOWN, unknown_contender_id)]
         )
 
         # Pick a proof and poll for its status (not a winner since mock time has not
         # advanced past the staking rewards minimum registration delay)
         manual_winner = quorum[1].proof
         contender_id = make_contender_id(tip, manual_winner.proofid)
         poll_node.send_poll([contender_id], inv_type=MSG_AVA_STAKE_CONTENDER)
         assert_response(
             [AvalancheVote(AvalancheContenderVoteError.PENDING, contender_id)]
         )
 
         # Advance time past the staking rewards minimum registration delay and
         # mine a block.
         now += 60 * 60 + 1
         node.setmocktime(now)
         tip = self.generate(node, 1)[0]
 
         # Staking rewards has been computed
-        # TODO When implemented, the response could be ACCEPTED if this proof was
-        # selected as a winner. For now, it is always INVALID.
         contender_id = make_contender_id(tip, manual_winner.proofid)
         poll_node.send_poll([contender_id], inv_type=MSG_AVA_STAKE_CONTENDER)
-        assert_response(
-            [AvalancheVote(AvalancheContenderVoteError.INVALID, contender_id)]
-        )
+        expectedVote = AvalancheContenderVoteError.INVALID
+        if node.getstakingreward(tip)[0]["proofid"] == uint256_hex(
+            manual_winner.proofid
+        ):
+            # If manual_winner happens to be selected as the winner, it will be accepted
+            expectedVote = AvalancheContenderVoteError.ACCEPTED
+        assert_response([AvalancheVote(expectedVote, contender_id)])
 
         # Manually set this contender as a winner
         node.setstakingreward(tip, manual_winner.payout_script.hex())
         poll_node.send_poll([contender_id], inv_type=MSG_AVA_STAKE_CONTENDER)
         assert_response(
             [AvalancheVote(AvalancheContenderVoteError.ACCEPTED, contender_id)]
         )
 
 
 if __name__ == "__main__":
     AvalancheContenderVotingTest().main()