diff --git a/src/avalanche/peermanager.h b/src/avalanche/peermanager.h --- a/src/avalanche/peermanager.h +++ b/src/avalanche/peermanager.h @@ -197,6 +197,12 @@ */ std::unordered_set m_unbroadcast_proofids; + /** + * Quorum management. + */ + uint32_t registeredScore = 0; + uint32_t allocatedScore = 0; + public: /** * Node API. @@ -310,6 +316,12 @@ void removeUnbroadcastProof(const ProofId &proofid); auto getUnbroadcastProofs() const { return m_unbroadcast_proofids; } + /* + * Quorum management + */ + uint32_t getRegisteredScore() const { return registeredScore; } + uint32_t getAllocatedScore() const { return allocatedScore; } + /**************************************************** * Functions which are public for testing purposes. * ****************************************************/ diff --git a/src/avalanche/peermanager.cpp b/src/avalanche/peermanager.cpp --- a/src/avalanche/peermanager.cpp +++ b/src/avalanche/peermanager.cpp @@ -74,6 +74,9 @@ const uint64_t start = slotCount; slots.emplace_back(start, score, it->peerid); slotCount = start + score; + + // Add to our allocated score when we allocate a new peer in the slots + allocatedScore += score; }); } @@ -120,9 +123,13 @@ return true; } - // There are no more node left, we need to cleanup. + // There are no more nodes left, we need to clean up. Subtract allocated + // score and remove from slots. const size_t i = it->index; assert(i < slots.size()); + assert(allocatedScore >= slots[i].getScore()); + allocatedScore -= slots[i].getScore(); + if (i + 1 == slots.size()) { slots.pop_back(); slotCount = slots.empty() ? 0 : slots.back().getStop(); @@ -311,6 +318,9 @@ auto inserted = peers.emplace(peerid, proof, nextCooldownTimePoint); assert(inserted.second); + // Add to our registered score when adding to the peer list + registeredScore += proof->getScore(); + // If there are nodes waiting for this proof, add them auto &pendingNodesView = pendingNodes.get(); auto range = pendingNodesView.equal_range(proofid); @@ -500,6 +510,10 @@ m_unbroadcast_proofids.erase(it->getProofId()); + // Remove the peer from the PeerSet and remove its score from the registered + // score total. + assert(registeredScore >= it->getScore()); + registeredScore -= it->getScore(); peers.erase(it); return true; } @@ -554,6 +568,7 @@ bool PeerManager::verify() const { uint64_t prevStop = 0; + uint32_t scoreFromSlots = 0; for (size_t i = 0; i < slots.size(); i++) { const Slot &s = slots[i]; @@ -574,10 +589,24 @@ if (it == peers.end() || it->index != i) { return false; } + + // Accumulate score across slots + scoreFromSlots += slots[i].getScore(); + } + + // Score across slots must be the same as our allocated score + if (scoreFromSlots != allocatedScore) { + return false; } + uint32_t scoreFromAllPeers = 0; + uint32_t scoreFromPeersWithNodes = 0; + std::unordered_set peersUtxos; for (const auto &p : peers) { + // Accumulate the score across peers to compare with total known score + scoreFromAllPeers += p.getScore(); + // A peer should have a proof attached if (!p.proof) { return false; @@ -628,6 +657,7 @@ continue; } + scoreFromPeersWithNodes += p.getScore(); // The index must point to a slot refering to this peer. if (p.index >= slots.size() || slots[p.index].getPeerId() != p.peerid) { return false; @@ -639,6 +669,14 @@ } } + // Check our accumulated scores against our registred and allocated scores + if (scoreFromAllPeers != registeredScore) { + return false; + } + if (scoreFromPeersWithNodes != allocatedScore) { + return false; + } + // We checked the utxo consistency for all our peers utxos already, so if // the pool size differs from the expected one there are dangling utxos. if (validProofPool.size() != peersUtxos.size()) { diff --git a/src/avalanche/test/peermanager_tests.cpp b/src/avalanche/test/peermanager_tests.cpp --- a/src/avalanche/test/peermanager_tests.cpp +++ b/src/avalanche/test/peermanager_tests.cpp @@ -1565,4 +1565,185 @@ expectedScores.begin(), expectedScores.end()); } +BOOST_FIXTURE_TEST_CASE(registered_score_tracking, NoCoolDownFixture) { + avalanche::PeerManager pm; + + const CKey key = CKey::MakeCompressedKey(); + + const Amount amount10(10 * COIN); + const Amount amount20(20 * COIN); + const Amount amount30 = amount10 + amount20; + const uint32_t height = 100; + const bool is_coinbase = false; + CScript script = GetScriptForDestination(PKHash(key.GetPubKey())); + + const COutPoint conflictingOutpoint(TxId(GetRandHash()), 0); + { + LOCK(cs_main); + CCoinsViewCache &coins = ::ChainstateActive().CoinsTip(); + coins.AddCoin(conflictingOutpoint, + Coin(CTxOut(amount10, script), height, is_coinbase), + false); + } + + const COutPoint secondaryOutpoint(TxId(GetRandHash()), 1); + { + LOCK(cs_main); + CCoinsViewCache &coins = ::ChainstateActive().CoinsTip(); + coins.AddCoin(secondaryOutpoint, + Coin(CTxOut(amount20, script), height, is_coinbase), + false); + } + + auto buildProofWithSequenceAndOutpoints = + [&](int64_t sequence, + const std::vector> &outpoints) { + ProofBuilder pb(sequence, 0, key); + for (const auto &outpoint : outpoints) { + BOOST_CHECK(pb.addUTXO(std::get<0>(outpoint), + std::get<1>(outpoint), height, + is_coinbase, key)); + } + return pb.build(); + }; + + auto proofSeq10 = buildProofWithSequenceAndOutpoints( + 10, {{conflictingOutpoint, amount10}, {secondaryOutpoint, amount20}}); + auto proofSeq20 = buildProofWithSequenceAndOutpoints( + 20, {{conflictingOutpoint, amount10}}); + auto orphan30 = buildProofWithSequenceAndOutpoints( + 30, {{conflictingOutpoint, amount10}, + {COutPoint{TxId(GetRandHash()), 0}, amount10}}); + + // Add proof and check that we have its score tracked + BOOST_CHECK_EQUAL(pm.getRegisteredScore(), 0); + BOOST_CHECK(pm.registerProof(proofSeq20)); + BOOST_CHECK_EQUAL(pm.getRegisteredScore(), Proof::amountToScore(amount10)); + + // Ensure failing to add new proofs doesn't affect the score + BOOST_CHECK(!pm.registerProof(proofSeq10)); + BOOST_CHECK(!pm.registerProof(orphan30)); + + BOOST_CHECK(pm.isBoundToPeer(proofSeq20->getId())); + BOOST_CHECK(pm.isInConflictingPool(proofSeq10->getId())); + BOOST_CHECK(pm.isOrphan(orphan30->getId())); + + BOOST_CHECK_EQUAL(pm.getRegisteredScore(), Proof::amountToScore(amount10)); + + auto checkRejectDefault = [&](const ProofId &proofid) { + BOOST_CHECK(pm.exists(proofid)); + const bool isOrphan = pm.isOrphan(proofid); + BOOST_CHECK(pm.rejectProof( + proofid, avalanche::PeerManager::RejectionMode::DEFAULT)); + BOOST_CHECK(!pm.isBoundToPeer(proofid)); + BOOST_CHECK_EQUAL(pm.exists(proofid), !isOrphan); + }; + + auto checkRejectInvalidate = [&](const ProofId &proofid) { + BOOST_CHECK(pm.exists(proofid)); + BOOST_CHECK(pm.rejectProof( + proofid, avalanche::PeerManager::RejectionMode::INVALIDATE)); + }; + + // Reject from the orphan pool doesn't affect tracked score + checkRejectDefault(orphan30->getId()); + BOOST_CHECK(!pm.registerProof(orphan30)); + BOOST_CHECK(pm.isOrphan(orphan30->getId())); + BOOST_CHECK_EQUAL(pm.getRegisteredScore(), Proof::amountToScore(amount10)); + checkRejectInvalidate(orphan30->getId()); + BOOST_CHECK_EQUAL(pm.getRegisteredScore(), Proof::amountToScore(amount10)); + + // Reject from the conflicting pool + checkRejectDefault(proofSeq10->getId()); + checkRejectInvalidate(proofSeq10->getId()); + + // Add again a proof to the conflicting pool + BOOST_CHECK(!pm.registerProof(proofSeq10)); + BOOST_CHECK(pm.isInConflictingPool(proofSeq10->getId())); + BOOST_CHECK_EQUAL(pm.getRegisteredScore(), Proof::amountToScore(amount10)); + + // Reject from the valid pool, default mode + // Now the score should change as the new peer is promoted + checkRejectDefault(proofSeq20->getId()); + BOOST_CHECK(!pm.isInConflictingPool(proofSeq10->getId())); + BOOST_CHECK(pm.isBoundToPeer(proofSeq10->getId())); + BOOST_CHECK_EQUAL(pm.getRegisteredScore(), Proof::amountToScore(amount30)); + + // Reject from the valid pool, invalidate mode + // Now the score should change as the old peer is re-promoted + checkRejectInvalidate(proofSeq10->getId()); + + // The conflicting proof should also be promoted to a peer + BOOST_CHECK(!pm.isInConflictingPool(proofSeq20->getId())); + BOOST_CHECK(pm.isBoundToPeer(proofSeq20->getId())); + BOOST_CHECK_EQUAL(pm.getRegisteredScore(), Proof::amountToScore(amount10)); + + // Trying to remove non-existent peer doesn't affect score + BOOST_CHECK(!pm.removePeer(1234)); + BOOST_CHECK_EQUAL(pm.getRegisteredScore(), Proof::amountToScore(amount10)); + + BOOST_CHECK(pm.removePeer(2)); + BOOST_CHECK_EQUAL(pm.getRegisteredScore(), 0); +} + +BOOST_AUTO_TEST_CASE(allocated_score_tracking) { + avalanche::PeerManager pm; + + // Create one peer. + auto proof = buildRandomProof(10000000 * MIN_VALID_PROOF_SCORE); + uint32_t score1 = 1000000000; + BOOST_CHECK(pm.registerProof(proof)); + BOOST_CHECK_EQUAL(pm.getRegisteredScore(), score1); + BOOST_CHECK_EQUAL(pm.getAllocatedScore(), 0); + + // Add 4 nodes. We should now have an allocated score but it shouldn't + // matter how many nodes we add. + const ProofId &proofid = proof->getId(); + for (int i = 0; i < 4; i++) { + BOOST_CHECK(pm.addNode(i, proofid)); + BOOST_CHECK_EQUAL(pm.getRegisteredScore(), score1); + BOOST_CHECK_EQUAL(pm.getAllocatedScore(), score1); + } + + // Remove a node, check that it doesn't change the score + BOOST_CHECK(pm.removeNode(2)); + BOOST_CHECK_EQUAL(pm.getRegisteredScore(), score1); + BOOST_CHECK_EQUAL(pm.getAllocatedScore(), score1); + + // Create a new peer. Without a node it doesn't affect allocated score and + // after adding a node it does. + uint32_t score2 = 100; + proof = buildRandomProof(MIN_VALID_PROOF_SCORE); + BOOST_CHECK(pm.registerProof(proof)); + BOOST_CHECK_EQUAL(pm.getRegisteredScore(), score1 + score2); + BOOST_CHECK_EQUAL(pm.getAllocatedScore(), score1); + BOOST_CHECK(pm.addNode(3, proof->getId())); + BOOST_CHECK_EQUAL(pm.getRegisteredScore(), score1 + score2); + BOOST_CHECK_EQUAL(pm.getAllocatedScore(), score1 + score2); + + // The first peer has two nodes left. Remove one and nothing happens, remove + // the other and its score is no longer allocated. + BOOST_CHECK(pm.removeNode(0)); + BOOST_CHECK_EQUAL(pm.getRegisteredScore(), score1 + score2); + BOOST_CHECK_EQUAL(pm.getAllocatedScore(), score1 + score2); + BOOST_CHECK(pm.removeNode(1)); + BOOST_CHECK_EQUAL(pm.getRegisteredScore(), score1 + score2); + BOOST_CHECK_EQUAL(pm.getAllocatedScore(), score2); + + // Removing a peer with no allocated score has no affect. + BOOST_CHECK(pm.removePeer(0)); + BOOST_CHECK_EQUAL(pm.getRegisteredScore(), score2); + BOOST_CHECK_EQUAL(pm.getAllocatedScore(), score2); + + // Remove the second peer's last node remove's its allocated score. + BOOST_CHECK(pm.removeNode(3)); + BOOST_CHECK_EQUAL(pm.getRegisteredScore(), score2); + BOOST_CHECK_EQUAL(pm.getAllocatedScore(), 0); + + // Removing the last peer takes us back to 0. + BOOST_CHECK(pm.removePeer(1)); + BOOST_CHECK_EQUAL(pm.getRegisteredScore(), 0); + BOOST_CHECK_EQUAL(pm.getAllocatedScore(), 0); +} + BOOST_AUTO_TEST_SUITE_END()