diff --git a/src/avalanche/processor.cpp b/src/avalanche/processor.cpp --- a/src/avalanche/processor.cpp +++ b/src/avalanche/processor.cpp @@ -494,6 +494,13 @@ auto &vr = it->second; if (!vr.registerVote(nodeid, v.GetError())) { + if (vr.isInconclusive()) { + // Vote was inconclusive for too long. Reject it. + updates.emplace_back(item, VoteStatus::Rejected); + voteRecordsWriteView->erase(it); + return; + } + // This vote did not provide any extra information, move on. continue; } diff --git a/src/avalanche/voterecord.h b/src/avalanche/voterecord.h --- a/src/avalanche/voterecord.h +++ b/src/avalanche/voterecord.h @@ -16,6 +16,12 @@ */ static constexpr int AVALANCHE_FINALIZATION_SCORE = 128; +/** + * Number of votes allowable before a record is rejected for being inconclusive + * for too long. + */ +static constexpr int AVALANCHE_VOTE_INCONCLUSIVE_THRESHOLD = 1024; + /** * How many inflight requests can exist for one item. */ @@ -70,6 +76,10 @@ return getConfidence() >= AVALANCHE_FINALIZATION_SCORE; } + bool isInconclusive() const { + return successfulVotes > AVALANCHE_VOTE_INCONCLUSIVE_THRESHOLD; + } + /** * Register a new vote for an item and update confidence accordingly. * Returns true if the acceptance or finalization state changed. 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 @@ -114,6 +114,7 @@ self.poll_tests(node) self.update_tests(node) self.vote_tests(node) + self.reject_inconclusive_proof_tests(node) def poll_tests(self, node): proof_seq10 = self.build_conflicting_proof(node, 10) @@ -190,7 +191,7 @@ peer.wait_for_disconnect() def update_tests(self, node): - # Restart the node to get rid og in-flight requests + # Restart the node to get rid of in-flight requests self.restart_node(0) mock_time = int(time.time()) @@ -414,6 +415,45 @@ AvalancheVote(AvalancheProofVoteResponse.CONFLICT, proof_3_id), AvalancheVote(AvalancheProofVoteResponse.REJECTED, proof_4_id)]) + def reject_inconclusive_proof_tests(self, node): + # Restart the node to get rid of in-flight requests + self.restart_node(0) + + mock_time = int(time.time()) + node.setmocktime(mock_time) + + self.quorum = self.get_quorum(node) + peer = get_ava_p2p_interface(node) + + proof_seq1 = self.build_conflicting_proof(node, 1) + proof_seq2 = self.build_conflicting_proof(node, 2) + proofid_seq1 = avalanche_proof_from_hex(proof_seq1).proofid + proofid_seq2 = avalanche_proof_from_hex(proof_seq2).proofid + + node.sendavalancheproof(proof_seq2) + self.wait_until(lambda: proofid_seq2 in get_proof_ids(node)) + + assert proofid_seq2 in get_proof_ids(node) + assert proofid_seq1 not in get_proof_ids(node) + + mock_time += self.conflicting_proof_cooldown + node.setmocktime(mock_time) + + peer.send_avaproof(avalanche_proof_from_hex(proof_seq1)) + + # Wait until proof_seq1 is rejected + retry = 5 + while retry > 0: + try: + with node.assert_debug_log([f"Avalanche rejected proof {proofid_seq1:0{64}x}"]): + self.wait_until(lambda: not self.can_find_proof_in_poll( + proofid_seq1, response=AvalancheProofVoteResponse.UNKNOWN)) + break + except AssertionError: + retry -= 1 + + assert_greater_than(retry, 0) + if __name__ == '__main__': AvalancheProofVotingTest().main()