diff --git a/src/avalanche/avalanche.h b/src/avalanche/avalanche.h --- a/src/avalanche/avalanche.h +++ b/src/avalanche/avalanche.h @@ -24,6 +24,16 @@ */ static constexpr bool AVALANCHE_DEFAULT_PEER_DISCOVERY_ENABLED = false; +/** + * Is proof replacement enabled by default. + */ +static constexpr bool AVALANCHE_DEFAULT_PROOF_REPLACEMENT_ENABLED = false; + +/** + * Proof replacement cooldown time default value in seconds. + */ +static constexpr size_t AVALANCHE_DEFAULT_PROOF_REPLACEMENT_COOLDOWN = 60 * 60; + /** * Avalanche default cooldown in milliseconds. */ diff --git a/src/avalanche/peermanager.h b/src/avalanche/peermanager.h --- a/src/avalanche/peermanager.h +++ b/src/avalanche/peermanager.h @@ -256,6 +256,7 @@ bool isOrphan(const ProofId &proofid) const; private: + void moveToOrphan(const ProofRef &proof); bool createPeer(const ProofRef &proof); bool addOrUpdateNode(const PeerSet::iterator &it, NodeId nodeid); bool addNodeToPeer(const PeerSet::iterator &it); diff --git a/src/avalanche/peermanager.cpp b/src/avalanche/peermanager.cpp --- a/src/avalanche/peermanager.cpp +++ b/src/avalanche/peermanager.cpp @@ -4,6 +4,7 @@ #include <avalanche/peermanager.h> +#include <avalanche/avalanche.h> #include <avalanche/delegation.h> #include <avalanche/validation.h> #include <random.h> @@ -251,10 +252,32 @@ return orphanProofPool.getProof(proofid) != nullptr; } +void PeerManager::moveToOrphan(const ProofRef &proof) { + auto &peersView = peers.get<by_proofid>(); + auto it = peersView.find(proof->getId()); + if (it != peersView.end()) { + removePeer(it->peerid); + } + + orphanProofPool.addProofIfPreferred(proof); +} + bool PeerManager::createPeer(const ProofRef &proof) { assert(proof); - switch (validProofPool.addProofIfNoConflict(proof)) { + ProofPool::ConflictingProofSet conflictingProofs; + // Set a cooldown time to limit the proof replacement rate + std::chrono::seconds cooldown{ + gArgs.GetArg("-avalancheproofreplacementcooldown", + AVALANCHE_DEFAULT_PROOF_REPLACEMENT_COOLDOWN)}; + auto added = + gArgs.GetBoolArg("-enableavalancheproofreplacement", + AVALANCHE_DEFAULT_PROOF_REPLACEMENT_ENABLED) + ? validProofPool.addProofIfPreferred(proof, conflictingProofs, + cooldown) + : validProofPool.addProofIfNoConflict(proof, conflictingProofs); + + switch (added) { case ProofPool::AddProofStatus::REJECTED: // The proof has conflicts, orphan the proof so it can be pulled // back if the conflicting ones are invalidated. @@ -269,6 +292,12 @@ // No default case, so the compiler can warn about missing cases } + // If we have overridden other proofs due to conflict, remove the peers and + // attempt to orphan them + for (const ProofRef &conflictingProof : conflictingProofs) { + moveToOrphan(conflictingProof); + } + // New peer means new peerid! const PeerId peerid = nextPeerId++; 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 @@ -15,6 +15,8 @@ #include <boost/test/unit_test.hpp> +#include <tinyformat.h> + using namespace avalanche; namespace avalanche { @@ -859,8 +861,6 @@ } BOOST_AUTO_TEST_CASE(conflicting_proof_selection) { - avalanche::PeerManager pm; - const CKey key = CKey::MakeCompressedKey(); const Amount amount(10 * COIN); @@ -892,11 +892,23 @@ auto proof_base = buildProofWithSequence(10); + gArgs.ForceSetArg("-enableavalancheproofreplacement", "1"); + gArgs.ForceSetArg("avalancheproofreplacementcooldown", "0"); + ConflictingProofComparator comparator; auto checkPreferred = [&](const ProofRef &candidate, const ProofRef &reference, bool expectAccepted) { BOOST_CHECK_EQUAL(comparator(candidate, reference), expectAccepted); BOOST_CHECK_EQUAL(comparator(reference, candidate), !expectAccepted); + + avalanche::PeerManager pm; + BOOST_CHECK(pm.registerProof(reference)); + BOOST_CHECK(pm.isBoundToPeer(reference->getId())); + + BOOST_CHECK_EQUAL(pm.registerProof(candidate), expectAccepted); + BOOST_CHECK_EQUAL(pm.isBoundToPeer(candidate->getId()), expectAccepted); + BOOST_CHECK_EQUAL(pm.isBoundToPeer(reference->getId()), + !expectAccepted); }; // Same master key, lower sequence number @@ -940,6 +952,115 @@ checkPreferred(proofSimilar, proof_multiUtxo, proofSimilar->getId() < proof_multiUtxo->getId()); } + + gArgs.ClearForcedArg("-enableavalancheproofreplacement"); + gArgs.ClearForcedArg("-avalancheproofreplacementcooldown"); +} + +BOOST_AUTO_TEST_CASE(conflicting_proofs_cooldown) { + avalanche::PeerManager pm; + + int64_t cooldown = 100; + + gArgs.ForceSetArg("-enableavalancheproofreplacement", "1"); + gArgs.ForceSetArg("avalancheproofreplacementcooldown", + strprintf("%d", cooldown)); + + int64_t mockTime = 100; + SetMockTime(mockTime); + + const CKey key = CKey::MakeCompressedKey(); + + const Amount amount(10 * COIN); + const uint32_t height = 100; + const bool is_coinbase = false; + + CScript script = GetScriptForDestination(PKHash(key.GetPubKey())); + + auto addCoin = [&](const Amount &amount) { + LOCK(cs_main); + const COutPoint outpoint(TxId(GetRandHash()), 0); + CCoinsViewCache &coins = ::ChainstateActive().CoinsTip(); + coins.AddCoin(outpoint, + Coin(CTxOut(amount, script), height, is_coinbase), false); + return outpoint; + }; + + auto conflictingOutpoint = addCoin(amount); + + auto buildProofWithSequence = [&](uint64_t sequence) { + ProofBuilder pb(sequence, GetRandInt(std::numeric_limits<int>::max()), + key); + BOOST_CHECK( + pb.addUTXO(conflictingOutpoint, amount, height, is_coinbase, key)); + + return pb.build(); + }; + + auto proof_seq10 = buildProofWithSequence(10); + auto proof_seq20 = buildProofWithSequence(20); + auto proof_seq30 = buildProofWithSequence(30); + + // No conflict means no cooldown + BOOST_CHECK(pm.registerProof(proof_seq10)); + BOOST_CHECK(pm.isBoundToPeer(proof_seq10->getId())); + BOOST_CHECK(!pm.isOrphan(proof_seq10->getId())); + + // Cooldown not elapsed, the proof is orphaned and no replacement occurs + mockTime += cooldown - 1; + SetMockTime(mockTime); + BOOST_CHECK(!pm.registerProof(proof_seq30)); + + BOOST_CHECK(pm.isBoundToPeer(proof_seq10->getId())); + BOOST_CHECK(!pm.isBoundToPeer(proof_seq30->getId())); + + BOOST_CHECK(!pm.isOrphan(proof_seq10->getId())); + BOOST_CHECK(pm.isOrphan(proof_seq30->getId())); + + // Cooldown elapsed, replacement can occur. Since there is already a better + // orphan candidate, proof_seq10 is evicted. + mockTime += 1; + SetMockTime(mockTime); + BOOST_CHECK(pm.registerProof(proof_seq20)); + + BOOST_CHECK(!pm.isBoundToPeer(proof_seq10->getId())); + BOOST_CHECK(pm.isBoundToPeer(proof_seq20->getId())); + BOOST_CHECK(!pm.isBoundToPeer(proof_seq30->getId())); + + BOOST_CHECK(!pm.isOrphan(proof_seq10->getId())); + BOOST_CHECK(!pm.isOrphan(proof_seq20->getId())); + BOOST_CHECK(pm.isOrphan(proof_seq30->getId())); + + // Mine a new block will NOT bring back proof_seq30 from orphan to a peer, + // because it is the best candidate but the cooldown is not elapsed since + // registration of proof_seq20 + pm.updatedBlockTip(); + + BOOST_CHECK(!pm.isBoundToPeer(proof_seq10->getId())); + BOOST_CHECK(pm.isBoundToPeer(proof_seq20->getId())); + BOOST_CHECK(!pm.isBoundToPeer(proof_seq30->getId())); + + BOOST_CHECK(!pm.isOrphan(proof_seq10->getId())); + BOOST_CHECK(!pm.isOrphan(proof_seq20->getId())); + BOOST_CHECK(pm.isOrphan(proof_seq30->getId())); + + // Wait for the cooldown and mine a block again. Now proof_seq30 will be + // promoted to peer and proof_seq20 will be orphaned. + mockTime += cooldown; + SetMockTime(mockTime); + + pm.updatedBlockTip(); + + BOOST_CHECK(!pm.isBoundToPeer(proof_seq10->getId())); + BOOST_CHECK(!pm.isBoundToPeer(proof_seq20->getId())); + BOOST_CHECK(pm.isBoundToPeer(proof_seq30->getId())); + + BOOST_CHECK(!pm.isOrphan(proof_seq10->getId())); + BOOST_CHECK(pm.isOrphan(proof_seq20->getId())); + BOOST_CHECK(!pm.isOrphan(proof_seq30->getId())); + + gArgs.ClearForcedArg("-enableavalancheproofreplacement"); + gArgs.ClearForcedArg("-avalancheproofreplacementcooldown"); } BOOST_AUTO_TEST_CASE(conflicting_orphans) { diff --git a/src/init.cpp b/src/init.cpp --- a/src/init.cpp +++ b/src/init.cpp @@ -1270,6 +1270,15 @@ strprintf("Enable avalanche peer discovery (default: %u)", AVALANCHE_DEFAULT_PEER_DISCOVERY_ENABLED), ArgsManager::ALLOW_ANY, OptionsCategory::AVALANCHE); + argsman.AddArg("-enableavalancheproofreplacement", + strprintf("Enable avalanche proof replacement (default: %u)", + AVALANCHE_DEFAULT_PROOF_REPLACEMENT_ENABLED), + ArgsManager::ALLOW_BOOL, OptionsCategory::AVALANCHE); + argsman.AddArg("-avalancheproofreplacementcooldown", + strprintf("Mandatory cooldown before a proof replacement " + "can occur in seconds (default: %u)", + AVALANCHE_DEFAULT_PROOF_REPLACEMENT_COOLDOWN), + ArgsManager::ALLOW_INT, OptionsCategory::AVALANCHE); argsman.AddArg( "-avacooldown", strprintf("Mandatory cooldown between two avapoll (default: %u)",