diff --git a/src/net_processing.cpp b/src/net_processing.cpp --- a/src/net_processing.cpp +++ b/src/net_processing.cpp @@ -4517,17 +4517,37 @@ LogPrint(BCLog::NET, "New avalanche proof: peer=%d, proofid %s\n", nodeid, proofid.ToString()); - } else { - // If the proof couldn't be added, it can be either orphan or - // invalid. In the latter case we should increase the ban score. - // TODO improve the ban reason by printing the validation state - if (!g_avalanche->withPeerManager([&](avalanche::PeerManager &pm) { - return pm.isOrphan(proofid); - })) { - WITH_LOCK(cs_rejectedProofs, rejectedProofs->insert(proofid)); - Misbehaving(nodeid, 100, "invalid-avaproof"); - } } + + // If the proof couldn't be added, it can be in the orphan pool, + // conflicting pool or invalid. In the latter case we should + // increase the ban score. + // TODO improve the ban reason by printing the validation state + if (!g_avalanche->withPeerManager([&](avalanche::PeerManager &pm) { + return pm.exists(proofid); + })) { + WITH_LOCK(cs_rejectedProofs, rejectedProofs->insert(proofid)); + Misbehaving(nodeid, 100, "invalid-avaproof"); + return; + } + + if (g_avalanche->withPeerManager([&](avalanche::PeerManager &pm) { + return pm.isOrphan(proofid); + })) { + LogPrint(BCLog::NET, + "New orphan avalanche proof, not polling: peer=%d, " + "proofid %s\n", + nodeid, proofid.ToString()); + return; + } + + // If proof replacement is not enabled, there is no point submitting the + // proof to the vote. + if (gArgs.GetBoolArg("-enableavalancheproofreplacement", + AVALANCHE_DEFAULT_PROOF_REPLACEMENT_ENABLED)) { + g_avalanche->addProofToReconcile(proof); + } + return; } diff --git a/test/functional/abc_p2p_avalanche_proof_voting.py b/test/functional/abc_p2p_avalanche_proof_voting.py new file mode 100755 --- /dev/null +++ b/test/functional/abc_p2p_avalanche_proof_voting.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python3 +# Copyright (c) 2021 The Bitcoin developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Test the resolution of conflicting proofs via avalanche.""" +from test_framework.avatools import ( + create_coinbase_stakes, + get_ava_p2p_interface, +) +from test_framework.key import ECKey, ECPubKey +from test_framework.messages import ( + AvalancheVote, + AvalancheVoteError, + FromHex, + LegacyAvalancheProof, +) +from test_framework.test_framework import BitcoinTestFramework +from test_framework.wallet_util import bytes_to_wif + +QUORUM_NODE_COUNT = 16 + + +class AvalancheProofVotingTest(BitcoinTestFramework): + def set_test_params(self): + self.setup_clean_chain = True + self.num_nodes = 1 + self.extra_args = [ + ['-enableavalanche=1', '-enableavalancheproofreplacement=1', + '-avalancheconflictingproofcooldown=0', '-avacooldown=0'], + ] + self.supports_cli = False + + def run_test(self): + node = self.nodes[0] + + # Build a fake quorum of nodes. + def get_quorum(): + return [get_ava_p2p_interface(node) + for _ in range(0, QUORUM_NODE_COUNT)] + + # Pick on node from the quorum for polling. + quorum = get_quorum() + + privkey = ECKey() + privkey.set(bytes.fromhex( + "12b004fff7f4b69ef8650e767f18f11ede158148b425660723b9f9a66e61f747"), True) + proof_master = privkey.get_pubkey().get_bytes().hex() + + addrkey0 = node.get_deterministic_priv_key() + blockhash = node.generatetoaddress(10, addrkey0.address) + stakes = create_coinbase_stakes(node, blockhash[:5], addrkey0.key) + + quorum_proof = node.buildavalancheproof( + 42, 0, bytes_to_wif(privkey.get_bytes()), stakes) + for n in quorum: + success = node.addavalanchenode( + n.nodeid, proof_master, quorum_proof) + assert success is True + + conflicting_stakes = create_coinbase_stakes( + node, blockhash[5:], addrkey0.key) + + def build_conflicting_proof(sequence): + return node.buildavalancheproof(sequence, 0, bytes_to_wif( + privkey.get_bytes()), conflicting_stakes) + + proof_seq10 = build_conflicting_proof(10) + proof_seq20 = build_conflicting_proof(20) + proof_seq30 = build_conflicting_proof(30) + proof_seq40 = build_conflicting_proof(40) + orphan = node.buildavalancheproof( + 40, 2000000000, bytes_to_wif(privkey.get_bytes()), [{ + 'txid': '0' * 64, + 'vout': 0, + 'amount': 10e6, + 'height': 42, + 'iscoinbase': False, + 'privatekey': bytes_to_wif(privkey.get_bytes()), + }] + ) + + # Get the key so we can verify signatures. + avakey = ECPubKey() + avakey.set(bytes.fromhex(node.getavalanchekey())) + + self.log.info("Trigger polling from the node...") + + def can_find_proof_in_poll(hash, resp=AvalancheVoteError.ACCEPTED): + found_hash = False + for n in quorum: + poll = n.get_avapoll_if_available() + + # That node has not received a poll + if poll is None: + continue + + # We got a poll, check for the hash and repond + votes = [] + for inv in poll.invs: + # Vote yes to everything + r = AvalancheVoteError.ACCEPTED + + # Look for what we expect + if inv.hash == hash: + r = resp + found_hash = True + + votes.append(AvalancheVote(r, inv.hash)) + + n.send_avaresponse(poll.round, votes, privkey) + + return found_hash + + peer = get_ava_p2p_interface(node) + + def send_and_check_for_polling(proof_hex): + proof = FromHex(LegacyAvalancheProof(), proof_hex) + peer.send_avaproof(proof) + self.wait_until(lambda: can_find_proof_in_poll(proof.proofid)) + + self.log.info("Check we poll for valid proof") + send_and_check_for_polling(proof_seq30) + + self.log.info( + "Check we poll for conflicting proof if the proof is not the favorite") + send_and_check_for_polling(proof_seq20) + + self.log.info( + "Check we poll for conflicting proof if the proof is the favorite") + send_and_check_for_polling(proof_seq40) + + self.log.info("Check we don't poll for orphans") + with node.assert_debug_log("New orphan avalanche proof, not polling"): + peer.send_avaproof(FromHex(LegacyAvalancheProof(), orphan)) + + self.log.info("Check we don't poll for proofs that get evicted") + with node.assert_debug_log("invalid-avaproof"): + peer.send_avaproof(FromHex(LegacyAvalancheProof(), proof_seq10)) + # We should get banned + peer.wait_for_disconnect() + + +if __name__ == '__main__': + AvalancheProofVotingTest().main() diff --git a/test/functional/test_framework/avatools.py b/test/functional/test_framework/avatools.py --- a/test/functional/test_framework/avatools.py +++ b/test/functional/test_framework/avatools.py @@ -14,6 +14,7 @@ NODE_AVALANCHE, NODE_NETWORK, AvalancheDelegation, + AvalancheProof, AvalancheResponse, CInv, CTransaction, @@ -24,6 +25,7 @@ hash256, msg_avahello, msg_avapoll, + msg_avaproof, msg_tcpavaresponse, ) from .p2p import P2PInterface, p2p_lock @@ -242,6 +244,11 @@ return delegation.proofid + def send_avaproof(self, proof: AvalancheProof): + msg = msg_avaproof() + msg.proof = proof + self.send_message(msg) + def get_ava_p2p_interface( node: TestNode,