diff --git a/src/avalanche/processor.h b/src/avalanche/processor.h --- a/src/avalanche/processor.h +++ b/src/avalanche/processor.h @@ -186,6 +186,8 @@ void addProofToReconcile(const ProofRef &proof); bool isAccepted(const CBlockIndex *pindex) const; bool isAccepted(const ProofRef &proof) const; + bool isInconclusive(const CBlockIndex *pindex) const; + bool isInconclusive(const ProofRef &proof) const; int getConfidence(const CBlockIndex *pindex) const; int getConfidence(const ProofRef &proof) const; diff --git a/src/avalanche/processor.cpp b/src/avalanche/processor.cpp --- a/src/avalanche/processor.cpp +++ b/src/avalanche/processor.cpp @@ -333,6 +333,26 @@ return it->second.isAccepted(); } +bool Processor::isInconclusive(const CBlockIndex *pindex) const { + auto r = blockVoteRecords.getReadView(); + auto it = r->find(pindex); + if (it == r.end()) { + return false; + } + + return it->second.isInconclusive(); +} + +bool Processor::isInconclusive(const ProofRef &proof) const { + auto r = proofVoteRecords.getReadView(); + auto it = r->find(proof); + if (it == r.end()) { + return false; + } + + return it->second.isInconclusive(); +} + int Processor::getConfidence(const CBlockIndex *pindex) const { auto r = blockVoteRecords.getReadView(); auto it = r->find(pindex); 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/src/net_processing.cpp b/src/net_processing.cpp --- a/src/net_processing.cpp +++ b/src/net_processing.cpp @@ -3439,6 +3439,12 @@ return 1; } + // We're polling for this block too, but the vote has been inconclusive for + // too long, so start rejecting it when peers poll us. + if (g_avalanche->isInconclusive(pindex)) { + return 4; + } + // Parked block if (pindex->nStatus.isOnParkedChain()) { return 2; @@ -3496,6 +3502,13 @@ } return g_avalanche->withPeerManager([&id](avalanche::PeerManager &pm) { + // We're polling for this proof too, but the vote has been inconclusive + // for too long, so start rejecting it when peers poll us. + const auto proof = pm.getProof(id); + if (g_avalanche->isInconclusive(proof)) { + return 4; + } + // The proof is actively bound to a peer if (pm.isBoundToPeer(id)) { return 0; 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,14 @@ self.poll_tests(node) self.update_tests(node) self.vote_tests(node) + # If majority votes are inconclusive, the proof should eventually + # invalidate + self.inconclusive_proof_tests( + node, "invalidated", AvalancheProofVoteResponse.INCONCLUSIVE) + # If majority votes are accepted when some node votes inconclusive, the + # proof should still eventually finalize + self.inconclusive_proof_tests( + node, "finalized", AvalancheProofVoteResponse.ACTIVE) def poll_tests(self, node): proof_seq10 = self.build_conflicting_proof(node, 10) @@ -190,7 +198,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 +422,70 @@ AvalancheVote(AvalancheProofVoteResponse.CONFLICT, proof_3_id), AvalancheVote(AvalancheProofVoteResponse.REJECTED, proof_4_id)]) + def inconclusive_proof_tests(self, node, expectedLog, quorumResponse): + # 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) + + def poll_and_check(proofid, expected): + peer.send_poll([proofid], MSG_AVA_PROOF) + response = peer.wait_for_avaresponse() + r = response.response + return repr(r.votes[0]) == repr(AvalancheVote(expected, proofid)) + + assert poll_and_check(proofid_seq1, AvalancheProofVoteResponse.UNKNOWN) + + peer.send_avaproof(avalanche_proof_from_hex(proof_seq1)) + + def reciprocate_poll(proofid, pollResponse, expected): + self.can_find_proof_in_poll(proofid, response=pollResponse) + return poll_and_check(proofid, expected) + + assert reciprocate_poll( + proofid_seq1, + AvalancheProofVoteResponse.UNKNOWN, + AvalancheProofVoteResponse.CONFLICT) + + # Wait until proof_seq1 becomes inconclusive + self.wait_until( + lambda: reciprocate_poll( + proofid_seq1, + AvalancheProofVoteResponse.UNKNOWN, + AvalancheProofVoteResponse.INCONCLUSIVE)) + + # Wait until proof_seq1 voting is completed. We start responding to polls as if the quorum nodes + # have come to the same conclusion as each other. + retry = 5 + while retry > 0: + try: + with node.assert_debug_log([f"Avalanche {expectedLog} proof {proofid_seq1:0{64}x}"]): + self.wait_until(lambda: not self.can_find_proof_in_poll( + proofid_seq1, response=quorumResponse)) + break + except AssertionError: + retry -= 1 + + assert_greater_than(retry, 0) + if __name__ == '__main__': AvalancheProofVotingTest().main() diff --git a/test/functional/abc_p2p_avalanche_voting.py b/test/functional/abc_p2p_avalanche_voting.py --- a/test/functional/abc_p2p_avalanche_voting.py +++ b/test/functional/abc_p2p_avalanche_voting.py @@ -218,6 +218,58 @@ self.wait_until(has_parked_new_tip, timeout=15) assert_equal(node.getbestblockhash(), fork_tip) + # Trigger polling again + fork_node.generatetoaddress(2, fork_address) + fork_tip = fork_node.getbestblockhash() + self.wait_until(lambda: parked_block(fork_tip), timeout=15) + + def reciprocate_poll(hash_to_find, pollResponse, expected): + can_find_block_in_poll(hash_to_find, resp=pollResponse) + + poll_node.send_poll([hash_to_find]) + response = poll_node.wait_for_avaresponse() + r = response.response + return repr(r.votes[0]) == repr( + AvalancheVote(expected, hash_to_find)) + + hash_to_find = int(fork_tip, 16) + self.wait_until( + lambda: reciprocate_poll( + hash_to_find, + AvalancheVoteError.UNKNOWN, + AvalancheVoteError.INCONCLUSIVE), + timeout=30) + + # Wait until the block becomes invalidated due to inconclusive voting + with node.assert_debug_log([f"Avalanche invalidated block {hash_to_find:0{64}x}"]): + self.wait_until( + lambda: not can_find_block_in_poll( + hash_to_find, + resp=AvalancheVoteError.INCONCLUSIVE), + timeout=15) + + # Trigger polling again + fork_node.generatetoaddress(2, fork_address) + fork_tip = fork_node.getbestblockhash() + self.wait_until(lambda: parked_block(fork_tip), timeout=15) + + hash_to_find = int(fork_tip, 16) + self.wait_until( + lambda: reciprocate_poll( + hash_to_find, + AvalancheVoteError.UNKNOWN, + AvalancheVoteError.INCONCLUSIVE), + timeout=30) + + # Wait until the block becomes finalized, despite node being + # inconclusive + with node.assert_debug_log([f"Avalanche finalized block {hash_to_find:0{64}x}"]): + self.wait_until( + lambda: not can_find_block_in_poll( + hash_to_find, + resp=AvalancheVoteError.ACCEPTED), + timeout=15) + self.log.info( "Check the node is discouraging unexpected avaresponses.") with node.assert_debug_log( diff --git a/test/functional/test_framework/messages.py b/test/functional/test_framework/messages.py --- a/test/functional/test_framework/messages.py +++ b/test/functional/test_framework/messages.py @@ -1026,6 +1026,7 @@ INVALID = 1 PARKED = 2 FORK = 3 + INCONCLUSIVE = 4 UNKNOWN = -1 MISSING = -2 PENDING = -3 @@ -1036,6 +1037,7 @@ REJECTED = 1 ORPHAN = 2 CONFLICT = 3 + INCONCLUSIVE = 4 UNKNOWN = -1