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,9 @@ return validProofPool.getRegistrationTime(proofid); } + bool acceptProof(const ProofRef &proof); + bool rejectProof(const ProofRef &proof); + template bool forPeer(const ProofId &proofid, Callable &&func) const { auto &pview = peers.get(); diff --git a/src/avalanche/peermanager.cpp b/src/avalanche/peermanager.cpp --- a/src/avalanche/peermanager.cpp +++ b/src/avalanche/peermanager.cpp @@ -152,6 +152,51 @@ return createPeer(proof); } +bool PeerManager::acceptProof(const ProofRef &proof) { + const ProofId &proofid = proof->getId(); + + if (isBoundToPeer(proofid)) { + // Nothing to do + return true; + } + + // Remove the conflicting proofs + auto &pview = peers.get(); + for (const SignedStake &ss : proof->getStakes()) { + auto conflictingProof = + validProofPool.getProof(ss.getStake().getUTXO()); + if (!conflictingProof) { + // No conflict on this UTXO + continue; + } + + auto it = pview.find(conflictingProof->getId()); + // Should alwayse be true, but use belt and suspenders + if (it != pview.end()) { + removePeer(it->peerid); + } + } + + return createPeer(proof); +} + +bool PeerManager::rejectProof(const ProofRef &proof) { + const ProofId &proofid = proof->getId(); + + if (!exists(proofid)) { + // Nothing to do; + return true; + } + + auto &pview = peers.get(); + auto it = pview.find(proof->getId()); + if (it != pview.end()) { + return removePeer(it->peerid); + } + + return orphanProofPool.removeProof(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 @@ -1093,4 +1093,140 @@ BOOST_CHECK(!pm.exists(orphan10->getId())); } +BOOST_AUTO_TEST_CASE(accept_reject_conflicting_proof) { + avalanche::PeerManager pm; + + const CKey key = CKey::MakeCompressedKey(); + + uint64_t sequence = 42; + const int64_t expiration = 0; + const Amount amount = 10 * COIN; + const uint32_t height = 100; + const bool is_coinbase = false; + + CScript script = GetScriptForDestination(PKHash(key.GetPubKey())); + + auto addCoin = [&]() { + LOCK(cs_main); + COutPoint outpoint(TxId(GetRandHash()), 0); + CCoinsViewCache &coins = ::ChainstateActive().CoinsTip(); + coins.AddCoin(outpoint, + Coin(CTxOut(amount, script), height, is_coinbase), false); + + return outpoint; + }; + + gArgs.ForceSetArg("-enableavalancheproofreplacement", "1"); + + // Create a bunch of proofs with a single utxo, and remember these utxos so + // we can create conflicting proofs + std::vector conflictingOutpoints; + std::set conflictingProofs; + for (size_t i = 0; i < 10; i++) { + COutPoint outpoint = addCoin(); + conflictingOutpoints.push_back(outpoint); + + ProofBuilder pb(sequence, expiration, key); + BOOST_CHECK( + pb.addUTXO(std::move(outpoint), amount, height, is_coinbase, key)); + auto proof = pb.build(); + + BOOST_CHECK(pm.registerProof(proof)); + + conflictingProofs.insert(std::move(proof)); + }; + + // Add a few more proofs for good measure + for (size_t i = 0; i < 5; i++) { + ProofBuilder pb(0, 0, key); + BOOST_CHECK(pb.addUTXO(addCoin(), amount, height, is_coinbase, key)); + BOOST_CHECK(pm.registerProof(pb.build())); + }; + + auto buildConflictingProofWithSequence = [&](uint64_t proofSequence) { + // Create a proof that conflicts with all the conflicting outpoints + ProofBuilder pb(proofSequence, expiration, key); + for (const COutPoint &outpoint : conflictingOutpoints) { + BOOST_CHECK(pb.addUTXO(outpoint, amount, height, is_coinbase, key)); + } + + // Add a few other utxos for good measure + for (size_t i = 0; i < 5; i++) { + BOOST_CHECK( + pb.addUTXO(addCoin(), amount, height, is_coinbase, key)); + }; + + return pb.build(); + }; + + auto lowSeqProof = buildConflictingProofWithSequence(sequence - 1); + // Orphaned because it has conflicts and is not the best candidate + BOOST_CHECK(!pm.registerProof(lowSeqProof)); + BOOST_CHECK(pm.isOrphan(lowSeqProof->getId())); + + auto checkAcceptance = [&](const ProofRef &proof) { + BOOST_CHECK(pm.acceptProof(proof)); + BOOST_CHECK(pm.isBoundToPeer(proof->getId())); + }; + + auto checkRejection = [&](const ProofRef &proof) { + BOOST_CHECK(pm.rejectProof(proof)); + BOOST_CHECK(!pm.exists(proof->getId())); + }; + + // Accept the proof + checkAcceptance(lowSeqProof); + + // The conflicting proofs are now evicted + for (const ProofRef &p : conflictingProofs) { + BOOST_CHECK(!pm.exists(p->getId())); + } + + // Accepting a few more times has no effect + for (size_t i = 0; i < 10; i++) { + checkAcceptance(lowSeqProof); + } + + // Reject the proof + checkRejection(lowSeqProof); + + // Rejecting a few more times has no effect + for (size_t i = 0; i < 10; i++) { + checkRejection(lowSeqProof); + } + + // Finally accept the proof + checkAcceptance(lowSeqProof); + + auto highSeqProof = buildConflictingProofWithSequence(sequence + 1); + + // The highSeqProof will be orphaned due to the registration cooldown + BOOST_CHECK(!pm.registerProof(highSeqProof)); + BOOST_CHECK(pm.isOrphan(highSeqProof->getId())); + + // Accept the proof + checkAcceptance(highSeqProof); + + // The lowSeqProof is now evicted + BOOST_CHECK(!pm.exists(lowSeqProof->getId())); + + // Accepting a few more times has no effect + for (size_t i = 0; i < 10; i++) { + checkAcceptance(highSeqProof); + } + + // Reject the proof + checkRejection(highSeqProof); + + // Rejecting a few more times has no effect + for (size_t i = 0; i < 10; i++) { + checkRejection(highSeqProof); + } + + // Sanity check that nothing is broken internally + BOOST_CHECK(pm.verify()); + + gArgs.ClearForcedArg("-enableavalancheproofreplacement"); +} + BOOST_AUTO_TEST_SUITE_END()