diff --git a/src/avalanche/peermanager.h b/src/avalanche/peermanager.h --- a/src/avalanche/peermanager.h +++ b/src/avalanche/peermanager.h @@ -32,6 +32,15 @@ namespace avalanche { +/** + * Maximum number of orphan proofs the peer manager will accept from the + * network. Under good conditions, this allows the node to collect relevant + * proofs during IBD. Note that reorgs can cause the orphan pool to + * temporarily exceed this limit. But a change in chaintip cause previously + * reorged proofs to be trimmed. + */ +static constexpr uint32_t AVALANCHE_MAX_ORPHAN_PROOFS = 4000; + class Delegation; namespace { diff --git a/src/avalanche/peermanager.cpp b/src/avalanche/peermanager.cpp --- a/src/avalanche/peermanager.cpp +++ b/src/avalanche/peermanager.cpp @@ -217,7 +217,18 @@ return proof->verify(validationState, ::ChainstateActive().CoinsTip()))) { if (isOrphanState(validationState)) { - orphanProofPool.addProofIfPreferred(proof); + // Only accept orphan proofs if there's room in the orphan pool. + auto status = orphanProofPool.addProofIfNoConflict(proof); + if (status != ProofPool::AddProofStatus::SUCCEED) { + // Attempt proof replacement + orphanProofPool.addProofIfPreferred(proof); + } else if (orphanProofPool.countProofs() > + AVALANCHE_MAX_ORPHAN_PROOFS) { + // Adding this proof exceeds the orphan pool limit, so remove + // it. + orphanProofPool.removeProof(proof->getId()); + } + return invalidate(ProofRegistrationResult::ORPHAN, "orphan-proof"); } 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 @@ -791,6 +791,80 @@ // The status of proof 1 and proof2 are unchanged checkOrphan(proof1, true); checkOrphan(proof2, false); + + // Track expected orphans so we can test them later + std::vector orphans; + orphans.push_back(proof1); + orphans.push_back(proof2); + orphans.push_back(proof3); + + // Fill up orphan pool to test the size limit + for (uint32_t i = 1; i < AVALANCHE_MAX_ORPHAN_PROOFS; i++) { + COutPoint outpoint = COutPoint(TxId(GetRandHash()), 0); + auto proof = + buildProofWithOutpoints(key, {outpoint}, 10 * COIN, key, 0, height); + registerOrphan(proof); + orphans.push_back(proof); + } + + // New orphans are rejected when the pool is full, even if they have higher + // proof scores. + { + COutPoint outpoint = COutPoint(TxId(GetRandHash()), 0); + auto proof = + buildProofWithOutpoints(key, {outpoint}, 20 * COIN, key, 0, height); + registerOrphan(proof); + BOOST_CHECK(!pm.exists(proof->getId())); + } + + // Replacement when the pool is full still works + { + auto proof = buildProofWithOutpoints(key, {outpoint1}, 10 * COIN, key, + 1, height); + registerOrphan(proof); + checkOrphan(proof, true); + BOOST_CHECK(!pm.exists(proof1->getId())); + orphans.push_back(proof); + orphans.erase(orphans.begin()); + } + + // Reorg so that some more proofs become orphans + { + LOCK(cs_main); + CCoinsViewCache &coins = ::ChainstateActive().CoinsTip(); + coins.SpendCoin(outpoint2); + coins.SpendCoin(outpoint3); + } + + pm.updatedBlockTip(); + + // New orphans are rejected when the pool is full, even if they have higher + // proof scores. + { + COutPoint outpoint = COutPoint(TxId(GetRandHash()), 0); + auto proof = + buildProofWithOutpoints(key, {outpoint}, 20 * COIN, key, 0, height); + registerOrphan(proof); + BOOST_CHECK(!pm.exists(proof->getId())); + } + + // Even though we've exceeded the orphan pool limit, the reorged proofs are + // still being tracked. + for (auto &proof : orphans) { + checkOrphan(proof, true); + } + + // Another block causes orphans to be trimmed to the limit + pm.updatedBlockTip(); + + int numOrphans = 0; + for (auto &proof : orphans) { + if (pm.exists(proof->getId())) { + checkOrphan(proof, true); + numOrphans++; + } + } + BOOST_CHECK_EQUAL(numOrphans, AVALANCHE_MAX_ORPHAN_PROOFS); } BOOST_AUTO_TEST_CASE(dangling_node) {