diff --git a/src/avalanche/peermanager.h b/src/avalanche/peermanager.h --- a/src/avalanche/peermanager.h +++ b/src/avalanche/peermanager.h @@ -205,6 +205,7 @@ * Proof and Peer related API. */ bool registerProof(const std::shared_ptr &proof); + bool registerConflictingProof(const std::shared_ptr &proof); bool exists(const ProofId &proofid) const { return getProof(proofid) != nullptr; } diff --git a/src/avalanche/peermanager.cpp b/src/avalanche/peermanager.cpp --- a/src/avalanche/peermanager.cpp +++ b/src/avalanche/peermanager.cpp @@ -146,6 +146,22 @@ return !getProof(proof->getId()) && getPeerId(proof) != NO_PEER; } +bool PeerManager::registerConflictingProof( + const std::shared_ptr &proof) { + for (const auto &s : proof->getStakes()) { + auto it = utxos.find(s.getStake().getUTXO()); + if (it != utxos.end()) { + removePeer(it->second); + } + } + + // If the proof ends up being invalid lately, we have removed the valid + // conflicting peers. This might be a deliberate action from the proof + // owner, or maybe the invalidity did not propagate the same over the + // network. In any case we want to follow the vote of the avalanche network. + return registerProof(proof); +} + NodeId PeerManager::selectNode() { for (int retry = 0; retry < SELECT_NODE_MAX_RETRY; retry++) { const PeerId p = selectPeer(); 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 @@ -795,4 +795,126 @@ !pm.registerProof(std::make_shared(std::move(badProof)))); } +BOOST_AUTO_TEST_CASE(register_conflicting_proof) { + avalanche::PeerManager pm; + + const CKey key = CKey::MakeCompressedKey(); + + std::vector outpoints(10); + for (size_t i = 0; i < outpoints.size(); i++) { + outpoints[i] = COutPoint(TxId(GetRandHash()), 0); + } + + const Amount amount(10 * COIN); + const uint32_t height = 100; + const bool is_coinbase = false; + + CScript script = GetScriptForDestination(PKHash(key.GetPubKey())); + + { + LOCK(cs_main); + CCoinsViewCache &coins = ::ChainstateActive().CoinsTip(); + for (auto &outpoint : outpoints) { + coins.AddCoin(outpoint, + Coin(CTxOut(amount, script), height, is_coinbase), + false); + } + } + + auto buildProofWithOutpoints = [&](std::vector outpoints) { + ProofBuilder pb(0, 0, key); + for (auto &outpoint : outpoints) { + BOOST_CHECK(pb.addUTXO(outpoint, amount, height, is_coinbase, key)); + } + return std::make_shared(pb.build()); + }; + + // Build a set of proofs with one outpoint each and register them all + auto registerNProofs = [&](size_t numofProofs) { + std::vector proofids; + proofids.reserve(numofProofs); + + for (size_t i = 0; i < numofProofs; i++) { + auto proof = buildProofWithOutpoints({outpoints[i]}); + BOOST_CHECK(pm.registerProof(proof)); + proofids.emplace_back(proof->getId()); + } + + BOOST_CHECK_EQUAL(proofids.size(), numofProofs); + return proofids; + }; + + { + auto proofids = registerNProofs(5); + + // This proof conflicts with all the previous ones + auto proof = buildProofWithOutpoints(outpoints); + BOOST_CHECK(!pm.registerProof(proof)); + BOOST_CHECK(pm.registerConflictingProof(proof)); + const ProofId conflictingProofId = proof->getId(); + + // All the previous conflicting peers have been erased + for (const ProofId &proofid : proofids) { + BOOST_CHECK_NE(conflictingProofId, proofid); + BOOST_CHECK( + !pm.forPeer(proofid, [](const Peer &p) { return true; })); + } + + // Cleanup + pm.forEachPeer([&](const Peer &peer) { pm.removePeer(peer.peerid); }); + } + + { + auto proofids = registerNProofs(5); + + // This proof conflicts with the first 2 proofs + auto proof = buildProofWithOutpoints({outpoints[0], outpoints[1]}); + BOOST_CHECK(!pm.registerProof(proof)); + BOOST_CHECK(pm.registerConflictingProof(proof)); + const ProofId conflictingProofId = proof->getId(); + + // The first 2 conflicting peers have been erased + for (size_t i = 0; i < 2; i++) { + BOOST_CHECK_NE(conflictingProofId, proofids[i]); + BOOST_CHECK( + !pm.forPeer(proofids[i], [](const Peer &p) { return true; })); + } + for (size_t i = 2; i < 5; i++) { + BOOST_CHECK_NE(conflictingProofId, proofids[i]); + BOOST_CHECK( + pm.forPeer(proofids[i], [](const Peer &p) { return true; })); + } + + // Cleanup + pm.forEachPeer([&](const Peer &peer) { pm.removePeer(peer.peerid); }); + } + + { + auto proofids = registerNProofs(5); + + // This proof conflicts with all the previous ones + auto proof = buildProofWithOutpoints(outpoints); + // But is invalid + { + LOCK(cs_main); + CCoinsViewCache &coins = ::ChainstateActive().CoinsTip(); + coins.SpendCoin(outpoints[6]); + } + + BOOST_CHECK(!pm.registerProof(proof)); + BOOST_CHECK(!pm.registerConflictingProof(proof)); + const ProofId conflictingProofId = proof->getId(); + + // All the previous conflicting peers have been erased + for (const ProofId &proofid : proofids) { + BOOST_CHECK_NE(conflictingProofId, proofid); + BOOST_CHECK( + !pm.forPeer(proofid, [](const Peer &p) { return true; })); + } + + // Cleanup + pm.forEachPeer([&](const Peer &peer) { pm.removePeer(peer.peerid); }); + } +} + BOOST_AUTO_TEST_SUITE_END()