diff --git a/src/rpc/avalanche.cpp b/src/rpc/avalanche.cpp --- a/src/rpc/avalanche.cpp +++ b/src/rpc/avalanche.cpp @@ -909,8 +909,13 @@ "The hex encoded proof matching the identifier."}, {RPCResult::Type::BOOL, "orphan", "Whether the proof is an orphan."}, - {RPCResult::Type::BOOL, "isBoundToPeer", + {RPCResult::Type::BOOL, "boundToPeer", "Whether the proof is bound to an avalanche peer."}, + {RPCResult::Type::BOOL, "conflicting", + "Whether the proof has a conflicting UTXO with an avalanche " + "peer."}, + {RPCResult::Type::BOOL, "finalized", + "Whether the proof is finalized by vote."}, }}, }, RPCExamples{HelpExampleRpc("getrawavalancheproof", "")}, @@ -926,10 +931,17 @@ bool isOrphan = false; bool isBoundToPeer = false; - auto proof = - g_avalanche->withPeerManager([&](avalanche::PeerManager &pm) { + bool conflicting = false; + bool finalized = false; + auto proof = g_avalanche->withPeerManager( + [&](const avalanche::PeerManager &pm) { isOrphan = pm.isOrphan(proofid); isBoundToPeer = pm.isBoundToPeer(proofid); + conflicting = pm.isInConflictingPool(proofid); + finalized = + pm.forPeer(proofid, [&](const avalanche::Peer &p) { + return p.hasFinalized; + }); return pm.getProof(proofid); }); @@ -943,7 +955,9 @@ ss << *proof; ret.pushKV("proof", HexStr(ss)); ret.pushKV("orphan", isOrphan); - ret.pushKV("isBoundToPeer", isBoundToPeer); + ret.pushKV("boundToPeer", isBoundToPeer); + ret.pushKV("conflicting", conflicting); + ret.pushKV("finalized", finalized); return ret; }, 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 @@ -5,6 +5,7 @@ """Test the resolution of conflicting proofs via avalanche.""" import time +from test_framework.authproxy import JSONRPCException from test_framework.avatools import ( avalanche_proof_from_hex, create_coinbase_stakes, @@ -20,12 +21,7 @@ AvalancheVote, ) from test_framework.test_framework import BitcoinTestFramework -from test_framework.util import ( - assert_equal, - assert_greater_than, - assert_raises_rpc_error, - try_rpc, -) +from test_framework.util import assert_equal, assert_raises_rpc_error, try_rpc from test_framework.wallet_util import bytes_to_wif QUORUM_NODE_COUNT = 16 @@ -244,18 +240,14 @@ self.log.info("Test the peer replacement rate limit") - # Wait until proof_seq30 is finalized - retry = 5 - while retry > 0: - try: - with node.assert_debug_log([f"Avalanche finalized proof {proofid_seq30:0{64}x}"]): - self.wait_until(lambda: not self.can_find_proof_in_poll( - proofid_seq30, response=AvalancheProofVoteResponse.ACTIVE)) - break - except AssertionError: - retry -= 1 + def vote_until_finalized(proofid): + self.can_find_proof_in_poll( + proofid, response=AvalancheProofVoteResponse.ACTIVE) + return node.getrawavalancheproof( + f"{proofid:0{64}x}").get("finalized", False) - assert_greater_than(retry, 0) + # Wait until proof_seq30 is finalized + self.wait_until(lambda: vote_until_finalized(proofid_seq30)) # Not enough assert self.conflicting_proof_cooldown < self.peer_replacement_cooldown @@ -462,20 +454,19 @@ mock_time += self.conflicting_proof_cooldown node.setmocktime(mock_time) + def vote_until_dropped(proofid): + self.can_find_proof_in_poll( + proofid, response=AvalancheProofVoteResponse.UNKNOWN) + try: + node.getrawavalancheproof(f"{proofid:0{64}x}") + return False + except JSONRPCException: + return True + peer.send_avaproof(avalanche_proof_from_hex(proof_seq1)) - # Wait until proof_seq1 voting goes stale - retry = 5 - while retry > 0: - 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=10) - break - except AssertionError: - retry -= 1 - - assert_greater_than(retry, 0) + with node.assert_debug_log([f"Avalanche stalled proof {proofid_seq1:0{64}x}"], timeout=60 * self.options.timeout_factor): + self.wait_until(lambda: vote_until_dropped(proofid_seq1)) # Verify that proof_seq2 was not replaced assert proofid_seq2 in get_proof_ids(node) diff --git a/test/functional/abc_p2p_proof_inventory.py b/test/functional/abc_p2p_proof_inventory.py --- a/test/functional/abc_p2p_proof_inventory.py +++ b/test/functional/abc_p2p_proof_inventory.py @@ -122,7 +122,7 @@ msg.proof = orphan peer.send_message(msg) - wait_for_proof(node, orphan_proofid, expect_orphan=True) + wait_for_proof(node, orphan_proofid, expect_status="orphan") def test_ban_invalid_proof(self): node = self.nodes[0] @@ -251,7 +251,7 @@ self.wait_until(lambda: proof_inv_received(peers)) # Sanity check our node knows the proof, and it is valid - wait_for_proof(node, proofid_hex, expect_orphan=False) + wait_for_proof(node, proofid_hex, expect_status="boundToPeer") # Mature the utxo then spend it node.generate(100) diff --git a/test/functional/abc_rpc_avalancheproof.py b/test/functional/abc_rpc_avalancheproof.py --- a/test/functional/abc_rpc_avalancheproof.py +++ b/test/functional/abc_rpc_avalancheproof.py @@ -8,6 +8,7 @@ from test_framework.address import ADDRESS_ECREG_UNSPENDABLE, base58_to_byte from test_framework.avatools import ( + avalanche_proof_from_hex, create_coinbase_stakes, create_stakes, get_proof_ids, @@ -21,6 +22,7 @@ AvalancheProof, FromHex, LegacyAvalancheProof, + msg_avaproof, ) from test_framework.p2p import P2PInterface, p2p_lock from test_framework.test_framework import BitcoinTestFramework @@ -214,11 +216,11 @@ self.connect_nodes(1, node.index) self.sync_all() self.nodes[1].generate(1) - wait_for_proof(self.nodes[1], proofid_hex, expect_orphan=True) + wait_for_proof(self.nodes[1], proofid_hex, expect_status="orphan") # Mine another block to make the orphan mature self.nodes[1].generate(1) - wait_for_proof(self.nodes[0], proofid_hex, expect_orphan=False) + wait_for_proof(self.nodes[0], proofid_hex, expect_status="boundToPeer") self.log.info( "Generate delegations for the proof, verify and decode them") @@ -449,6 +451,8 @@ stake_age = node.getblockcount() self.restart_node(0, self.extra_args[0] + [ "-avaproofstakeutxoconfirmations={}".format(stake_age), + '-enableavalancheproofreplacement=1', + '-avalancheconflictingproofcooldown=0' ]) # Good proof @@ -471,11 +475,33 @@ raw_proof = node.getrawavalancheproof("{:064x}".format(proofid)) assert_equal(raw_proof['proof'], proof) assert_equal(raw_proof['orphan'], False) - assert_equal(raw_proof['isBoundToPeer'], True) + assert_equal(raw_proof['boundToPeer'], True) + assert_equal(raw_proof['conflicting'], False) + assert_equal(raw_proof['finalized'], False) assert_raises_rpc_error(-8, "Proof not found", node.getrawavalancheproof, '0' * 64) + conflicting_proof = node.buildavalancheproof( + proof_sequence - 1, proof_expiration, wif_privkey, stakes) + conflicting_proofobj = avalanche_proof_from_hex(conflicting_proof) + conflicting_proofid_hex = f"{conflicting_proofobj.proofid:0{64}x}" + + msg = msg_avaproof() + msg.proof = conflicting_proofobj + peer.send_message(msg) + wait_for_proof( + node, + conflicting_proofid_hex, + expect_status="conflicting") + + raw_proof = node.getrawavalancheproof(conflicting_proofid_hex) + assert_equal(raw_proof['proof'], conflicting_proof) + assert_equal(raw_proof['orphan'], False) + assert_equal(raw_proof['boundToPeer'], False) + assert_equal(raw_proof['conflicting'], True) + assert_equal(raw_proof['finalized'], False) + # To orphan the proof, we make it immature by switching to a shorter # chain node.invalidateblock(node.getbestblockhash()) @@ -496,7 +522,9 @@ raw_proof = node.getrawavalancheproof("{:064x}".format(proofid)) assert_equal(raw_proof['proof'], proof) assert_equal(raw_proof['orphan'], True) - assert_equal(raw_proof['isBoundToPeer'], False) + assert_equal(raw_proof['boundToPeer'], False) + assert_equal(raw_proof['conflicting'], False) + assert_equal(raw_proof['finalized'], False) self.log.info("Bad proof should be rejected at startup") diff --git a/test/functional/p2p_inv_download.py b/test/functional/p2p_inv_download.py --- a/test/functional/p2p_inv_download.py +++ b/test/functional/p2p_inv_download.py @@ -391,7 +391,7 @@ "-avamasterkey={}".format(bytes_to_wif(privkey.get_bytes())), ]) node.generate(1) - wait_for_proof(node, proofid_hex, expect_orphan=True) + wait_for_proof(node, proofid_hex, expect_status="orphan") peer = node.add_p2p_connection(context.p2p_conn()) peer.send_message( 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 @@ -35,7 +35,7 @@ ) from .p2p import P2PInterface, p2p_lock from .test_node import TestNode -from .util import assert_equal, satoshi_round, wait_until_helper +from .util import satoshi_round, wait_until_helper from .wallet_util import bytes_to_wif @@ -153,22 +153,26 @@ return [int(peer['proofid'], 16) for peer in node.getavalanchepeerinfo()] -def wait_for_proof(node, proofid_hex, timeout=60, expect_orphan=None): +def wait_for_proof(node, proofid_hex, timeout=60, expect_status=None): """ - Wait for the proof to be known by the node. If expect_orphan is set, the - proof should match the orphan state, otherwise it's a don't care parameter. + Wait for the proof to be known by the node. If expect_status is set, the + proof should match the expected status, otherwise it's a don't care + parameter. expect_status can be one of the following: "orphan", + "boundToPeer", "conflicting" or "finalized". """ + ret = {} + def proof_found(): + nonlocal ret try: - wait_for_proof.is_orphan = node.getrawavalancheproof(proofid_hex)[ - "orphan"] + ret = node.getrawavalancheproof(proofid_hex) return True except JSONRPCException: return False wait_until_helper(proof_found, timeout=timeout) - if expect_orphan is not None: - assert_equal(expect_orphan, wait_for_proof.is_orphan) + if expect_status is not None: + assert ret.get(expect_status, False) is True class NoHandshakeAvaP2PInterface(P2PInterface):