diff --git a/src/avalanche/processor.h b/src/avalanche/processor.h --- a/src/avalanche/processor.h +++ b/src/avalanche/processor.h @@ -163,6 +163,10 @@ double minQuorumConnectedScoreRatio; std::atomic quorumIsEstablished{false}; + /** Voting parameters. */ + const uint32_t staleVoteThreshold; + const uint32_t staleVoteFactor; + /** Registered interfaces::Chain::Notifications handler. */ class NotificationsHandler; std::unique_ptr chainNotificationsHandler; @@ -170,7 +174,8 @@ Processor(const ArgsManager &argsman, interfaces::Chain &chain, CConnman *connmanIn, std::unique_ptr peerDataIn, CKey sessionKeyIn, uint32_t minQuorumTotalScoreIn, - double minQuorumConnectedScoreRatioIn); + double minQuorumConnectedScoreRatioIn, + uint32_t staleVoteThresholdIn, uint32_t staleVoteFactorIn); public: ~Processor(); diff --git a/src/avalanche/processor.cpp b/src/avalanche/processor.cpp --- a/src/avalanche/processor.cpp +++ b/src/avalanche/processor.cpp @@ -22,6 +22,7 @@ #include #include +#include #include /** @@ -139,14 +140,17 @@ Processor::Processor(const ArgsManager &argsman, interfaces::Chain &chain, CConnman *connmanIn, std::unique_ptr peerDataIn, CKey sessionKeyIn, uint32_t minQuorumTotalScoreIn, - double minQuorumConnectedScoreRatioIn) + double minQuorumConnectedScoreRatioIn, + uint32_t staleVoteThresholdIn, uint32_t staleVoteFactorIn) : connman(connmanIn), queryTimeoutDuration(argsman.GetArg( "-avatimeout", AVALANCHE_DEFAULT_QUERY_TIMEOUT.count())), round(0), peerManager(std::make_unique()), peerData(std::move(peerDataIn)), sessionKey(std::move(sessionKeyIn)), minQuorumScore(minQuorumTotalScoreIn), - minQuorumConnectedScoreRatio(minQuorumConnectedScoreRatioIn) { + minQuorumConnectedScoreRatio(minQuorumConnectedScoreRatioIn), + staleVoteThreshold(staleVoteThresholdIn), + staleVoteFactor(staleVoteFactorIn) { // Make sure we get notified of chain state changes. chainNotificationsHandler = chain.handleNotifications(std::make_shared(this)); @@ -279,10 +283,40 @@ return nullptr; } + // Determine voting parameters + int64_t staleVoteThreshold = argsman.GetArg("-avastalevotethreshold", + AVALANCHE_VOTE_STALE_THRESHOLD); + if (staleVoteThreshold < AVALANCHE_VOTE_STALE_MIN_THRESHOLD) { + error = strprintf(_("The avalanche stale vote threshold must be " + "greater than or equal to %d"), + AVALANCHE_VOTE_STALE_MIN_THRESHOLD); + return nullptr; + } + if (staleVoteThreshold > std::numeric_limits::max()) { + error = strprintf(_("The avalanche stale vote threshold must be less " + "than or equal to %d"), + std::numeric_limits::max()); + return nullptr; + } + + int64_t staleVoteFactor = + argsman.GetArg("-avastalevotefactor", AVALANCHE_VOTE_STALE_FACTOR); + if (staleVoteFactor <= 0) { + error = _("The avalanche stale vote factor must be greater than 0"); + return nullptr; + } + if (staleVoteFactor > std::numeric_limits::max()) { + error = strprintf(_("The avalanche stale vote factor must be less than " + "or equal to %d"), + std::numeric_limits::max()); + return nullptr; + } + // We can't use std::make_unique with a private constructor return std::unique_ptr(new Processor( argsman, chain, connman, std::move(peerData), std::move(sessionKey), - Proof::amountToScore(minQuorumStake), minQuorumConnectedStakeRatio)); + Proof::amountToScore(minQuorumStake), minQuorumConnectedStakeRatio, + staleVoteThreshold, staleVoteFactor)); } bool Processor::addBlockToReconcile(const CBlockIndex *pindex) { @@ -510,7 +544,7 @@ auto &vr = it->second; if (!vr.registerVote(nodeid, v.GetError())) { - if (vr.isStale()) { + if (vr.isStale(staleVoteThreshold, staleVoteFactor)) { updates.emplace_back(item, VoteStatus::Stale); // Just drop stale votes. If we see this item again, we'll diff --git a/src/avalanche/test/processor_tests.cpp b/src/avalanche/test/processor_tests.cpp --- a/src/avalanche/test/processor_tests.cpp +++ b/src/avalanche/test/processor_tests.cpp @@ -1292,4 +1292,90 @@ gArgs.ClearForcedArg("-avaminquorumconnectedstakeratio"); } +BOOST_AUTO_TEST_CASE_TEMPLATE(voting_parameters, P, VoteItemProviders) { + // Check that setting voting parameters has the expected effect + gArgs.ForceSetArg("-avastalevotethreshold", + ToString(AVALANCHE_VOTE_STALE_MIN_THRESHOLD)); + gArgs.ForceSetArg("-avastalevotefactor", "2"); + + std::vector> testCases = { + // {number of yes votes, number of neutral votes} + {0, AVALANCHE_VOTE_STALE_MIN_THRESHOLD}, + {AVALANCHE_FINALIZATION_SCORE + 4, AVALANCHE_FINALIZATION_SCORE - 6}, + }; + + bilingual_str error; + m_processor = Processor::MakeProcessor(*m_node.args, *m_node.chain, + m_node.connman.get(), error); + + BOOST_CHECK(m_processor != nullptr); + BOOST_CHECK(error.empty()); + + P provider(this); + auto &updates = provider.updates; + const uint32_t invType = provider.invType; + + const auto item = provider.buildVoteItem(); + const auto itemid = provider.getVoteItemId(item); + + // Create nodes that supports avalanche. + auto avanodes = ConnectNodes(); + int nextNodeIndex = 0; + + for (auto &testCase : testCases) { + // Add a new item. Check it is added to the polls. + BOOST_CHECK(provider.addToReconcile(item)); + auto invs = getInvsForNextPoll(); + BOOST_CHECK_EQUAL(invs.size(), 1); + BOOST_CHECK_EQUAL(invs[0].type, invType); + BOOST_CHECK(invs[0].hash == itemid); + + BOOST_CHECK(m_processor->isAccepted(item)); + + auto registerNewVote = [&](const Response &resp) { + runEventLoop(); + auto nodeid = avanodes[nextNodeIndex++ % avanodes.size()]->GetId(); + BOOST_CHECK(provider.registerVotes(nodeid, resp)); + }; + + // Add some confidence + for (int i = 0; i < std::get<0>(testCase); i++) { + Response resp = {getRound(), 0, {Vote(0, itemid)}}; + registerNewVote(next(resp)); + BOOST_CHECK(m_processor->isAccepted(item)); + BOOST_CHECK_EQUAL(m_processor->getConfidence(item), + i >= 6 ? i - 5 : 0); + BOOST_CHECK_EQUAL(updates.size(), 0); + } + + // Vote until just before item goes stale + for (int i = 0; i < std::get<1>(testCase); i++) { + Response resp = {getRound(), 0, {Vote(-1, itemid)}}; + registerNewVote(next(resp)); + BOOST_CHECK_EQUAL(updates.size(), 0); + } + + // As long as it is not stale, we poll. + invs = getInvsForNextPoll(); + BOOST_CHECK_EQUAL(invs.size(), 1); + BOOST_CHECK_EQUAL(invs[0].type, invType); + BOOST_CHECK(invs[0].hash == itemid); + + // Now stale + Response resp = {getRound(), 0, {Vote(-1, itemid)}}; + registerNewVote(next(resp)); + BOOST_CHECK_EQUAL(updates.size(), 1); + BOOST_CHECK(updates[0].getVoteItem() == item); + BOOST_CHECK(updates[0].getStatus() == VoteStatus::Stale); + updates.clear(); + + // Once stale, there is no poll for it. + invs = getInvsForNextPoll(); + BOOST_CHECK_EQUAL(invs.size(), 0); + } + + gArgs.ClearForcedArg("-avastalevotethreshold"); + gArgs.ClearForcedArg("-avastalevotefactor"); +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/src/avalanche/voterecord.h b/src/avalanche/voterecord.h --- a/src/avalanche/voterecord.h +++ b/src/avalanche/voterecord.h @@ -21,6 +21,12 @@ */ static constexpr uint32_t AVALANCHE_VOTE_STALE_THRESHOLD = 4096; +/** + * Lowest configurable staleness threshold (finalization score + necessary votes + * to increase confidence + wiggle room). + */ +static constexpr uint32_t AVALANCHE_VOTE_STALE_MIN_THRESHOLD = 140; + /** * Scaling factor applied to confidence to determine staleness threshold. * As confidence increases, the staleness threshold should as well. This @@ -82,9 +88,10 @@ return getConfidence() >= AVALANCHE_FINALIZATION_SCORE; } - bool isStale() const { - return successfulVotes > AVALANCHE_VOTE_STALE_THRESHOLD && - successfulVotes > getConfidence() * AVALANCHE_VOTE_STALE_FACTOR; + bool isStale(uint32_t staleThreshold = AVALANCHE_VOTE_STALE_THRESHOLD, + uint32_t staleFactor = AVALANCHE_VOTE_STALE_FACTOR) const { + return successfulVotes > staleThreshold && + successfulVotes > getConfidence() * staleFactor; } /** diff --git a/src/init.cpp b/src/init.cpp --- a/src/init.cpp +++ b/src/init.cpp @@ -15,6 +15,7 @@ #include #include // For AVALANCHE_LEGACY_PROOF_DEFAULT #include +#include // For AVALANCHE_VOTE_STALE_* #include #include #include @@ -1334,6 +1335,19 @@ " need nodes for to have a usable quorum (default: %s)", AVALANCHE_DEFAULT_MIN_QUORUM_CONNECTED_STAKE_RATIO), ArgsManager::ALLOW_STRING, OptionsCategory::AVALANCHE); + argsman.AddArg( + "-avastalevotethreshold", + strprintf("Number of avalanche votes before a voted item goes stale " + "when voting confidence is low (default: %u)", + AVALANCHE_VOTE_STALE_THRESHOLD), + ArgsManager::ALLOW_INT, OptionsCategory::AVALANCHE); + argsman.AddArg( + "-avastalevotefactor", + strprintf( + "Factor affecting the number of avalanche votes before a voted " + "item goes stale when voting confidence is high (default: %u)", + AVALANCHE_VOTE_STALE_FACTOR), + ArgsManager::ALLOW_INT, OptionsCategory::AVALANCHE); argsman.AddArg( "-avacooldown", strprintf("Mandatory cooldown between two avapoll (default: %u)", diff --git a/test/functional/abc_p2p_avalanche_proof_voting.py b/test/functional/abc_p2p_avalanche_proof_voting.py --- a/test/functional/abc_p2p_avalanche_proof_voting.py +++ b/test/functional/abc_p2p_avalanche_proof_voting.py @@ -38,7 +38,7 @@ self.peer_replacement_cooldown = 2000 self.extra_args = [ ['-enableavalanche=1', '-enableavalancheproofreplacement=1', - f'-avalancheconflictingproofcooldown={self.conflicting_proof_cooldown}', f'-avalanchepeerreplacementcooldown={self.peer_replacement_cooldown}', '-avacooldown=0'], + f'-avalancheconflictingproofcooldown={self.conflicting_proof_cooldown}', f'-avalanchepeerreplacementcooldown={self.peer_replacement_cooldown}', '-avacooldown=0', '-avastalevotethreshold=140', '-avastalevotefactor=1'], ] self.supports_cli = False @@ -447,7 +447,7 @@ try: with node.assert_debug_log([f"Avalanche stalled proof {proofid_seq1:0{64}x}"]): self.wait_until(lambda: not self.can_find_proof_in_poll( - proofid_seq1, response=AvalancheProofVoteResponse.UNKNOWN), timeout=30) + proofid_seq1, response=AvalancheProofVoteResponse.UNKNOWN), timeout=10) break except AssertionError: retry -= 1