diff --git a/test/functional/abc_p2p_avalanche_voting.py b/test/functional/abc_p2p_avalanche_voting.py index 69739b2ed..7f13c3c16 100755 --- a/test/functional/abc_p2p_avalanche_voting.py +++ b/test/functional/abc_p2p_avalanche_voting.py @@ -1,227 +1,227 @@ #!/usr/bin/env python3 # Copyright (c) 2020-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 forks via avalanche.""" import random from test_framework.avatools import get_ava_p2p_interface from test_framework.key import ECPubKey from test_framework.messages import AvalancheVote, AvalancheVoteError from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal, uint256_hex QUORUM_NODE_COUNT = 16 class AvalancheTest(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 2 self.extra_args = [ [ '-avalanche=1', '-avaproofstakeutxodustthreshold=1000000', '-avaproofstakeutxoconfirmations=1', '-avacooldown=0', '-avaminquorumstake=0', '-avaminavaproofsnodecount=0', '-whitelist=noban@127.0.0.1', ], [ '-avalanche=1', '-avaproofstakeutxoconfirmations=1', '-avacooldown=0', '-avaminquorumstake=0', '-avaminavaproofsnodecount=0', '-noparkdeepreorg', '-maxreorgdepth=-1', '-whitelist=noban@127.0.0.1', ], ] self.supports_cli = False self.rpc_timeout = 120 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. + # Pick one node from the quorum for polling. quorum = get_quorum() poll_node = quorum[0] assert node.getavalancheinfo()['ready_to_poll'] is True # Generate many block and poll for them. node.generate(100 - node.getblockcount()) fork_node = self.nodes[1] # Make sure the fork node has synced the blocks self.sync_blocks([node, fork_node]) # Get the key so we can verify signatures. avakey = ECPubKey() avakey.set(bytes.fromhex(node.getavalanchekey())) self.log.info("Poll for the chain tip...") best_block_hash = int(node.getbestblockhash(), 16) poll_node.send_poll([best_block_hash]) def assert_response(expected): response = poll_node.wait_for_avaresponse() r = response.response assert_equal(r.cooldown, 0) # Verify signature. assert avakey.verify_schnorr(response.sig, r.get_hash()) votes = r.votes assert_equal(len(votes), len(expected)) for i in range(0, len(votes)): assert_equal(repr(votes[i]), repr(expected[i])) assert_response( [AvalancheVote(AvalancheVoteError.ACCEPTED, best_block_hash)]) self.log.info("Poll for a selection of blocks...") various_block_hashes = [ int(node.getblockhash(0), 16), int(node.getblockhash(1), 16), int(node.getblockhash(10), 16), int(node.getblockhash(25), 16), int(node.getblockhash(42), 16), int(node.getblockhash(96), 16), int(node.getblockhash(99), 16), int(node.getblockhash(100), 16), ] poll_node.send_poll(various_block_hashes) assert_response([AvalancheVote(AvalancheVoteError.ACCEPTED, h) for h in various_block_hashes]) self.log.info( "Poll for a selection of blocks, but some are now invalid...") invalidated_block = node.getblockhash(76) node.invalidateblock(invalidated_block) # We need to send the coin to a new address in order to make sure we do # not regenerate the same block. node.generatetoaddress( 26, 'ecregtest:pqv2r67sgz3qumufap3h2uuj0zfmnzuv8v38gtrh5v') node.reconsiderblock(invalidated_block) poll_node.send_poll(various_block_hashes) assert_response([AvalancheVote(AvalancheVoteError.ACCEPTED, h) for h in various_block_hashes[:5]] + [AvalancheVote(AvalancheVoteError.FORK, h) for h in various_block_hashes[-3:]]) self.log.info("Poll for unknown blocks...") various_block_hashes = [ int(node.getblockhash(0), 16), int(node.getblockhash(25), 16), int(node.getblockhash(42), 16), various_block_hashes[5], various_block_hashes[6], various_block_hashes[7], random.randrange(1 << 255, (1 << 256) - 1), random.randrange(1 << 255, (1 << 256) - 1), random.randrange(1 << 255, (1 << 256) - 1), ] poll_node.send_poll(various_block_hashes) assert_response([AvalancheVote(AvalancheVoteError.ACCEPTED, h) for h in various_block_hashes[:3]] + [AvalancheVote(AvalancheVoteError.FORK, h) for h in various_block_hashes[3:6]] + [AvalancheVote(AvalancheVoteError.UNKNOWN, h) for h in various_block_hashes[-3:]]) self.log.info("Trigger polling from the node...") def can_find_block_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, n.delegated_privkey) return found_hash # Now that we have a peer, we should start polling for the tip. hash_tip = int(node.getbestblockhash(), 16) self.wait_until(lambda: can_find_block_in_poll(hash_tip), timeout=5) # Make sure the fork node has synced the blocks self.sync_blocks([node, fork_node]) # Create a fork 2 blocks deep. This should trigger polling. fork_node.invalidateblock(fork_node.getblockhash(100)) fork_address = fork_node.get_deterministic_priv_key().address fork_node.generatetoaddress(2, fork_address) # Because the new tip is a deep reorg, the node will not accept it # right away, but poll for it. def parked_block(blockhash): for tip in node.getchaintips(): if tip["hash"] == blockhash: assert tip["status"] != "active" return tip["status"] == "parked" return False fork_tip = fork_node.getbestblockhash() self.wait_until(lambda: parked_block(fork_tip)) self.log.info("Answer all polls to finalize...") hash_to_find = int(fork_tip, 16) def has_accepted_new_tip(): can_find_block_in_poll(hash_to_find) return node.getbestblockhash() == fork_tip # Because everybody answers yes, the node will accept that block. self.wait_until(has_accepted_new_tip, timeout=15) assert_equal(node.getbestblockhash(), fork_tip) self.log.info("Answer all polls to park...") node.generate(1) tip_to_park = node.getbestblockhash() hash_to_find = int(tip_to_park, 16) assert tip_to_park != fork_tip def has_parked_new_tip(): can_find_block_in_poll(hash_to_find, AvalancheVoteError.PARKED) return node.getbestblockhash() == fork_tip # Because everybody answers no, the node will park that block. with node.assert_debug_log([f"Avalanche invalidated block {uint256_hex(hash_to_find)}"]): self.wait_until(has_parked_new_tip, timeout=15) assert_equal(node.getbestblockhash(), fork_tip) self.log.info( "Check the node is discouraging unexpected avaresponses.") with node.assert_debug_log( ['Misbehaving', 'peer=1 (0 -> 2): unexpected-ava-response']): # unknown voting round poll_node.send_avaresponse( round=2**32 - 1, votes=[], privkey=poll_node.delegated_privkey) if __name__ == '__main__': AvalancheTest().main() diff --git a/test/functional/abc_rpc_isfinal.py b/test/functional/abc_rpc_isfinal.py index 5d8c61e01..9a52ce5b5 100755 --- a/test/functional/abc_rpc_isfinal.py +++ b/test/functional/abc_rpc_isfinal.py @@ -1,237 +1,237 @@ #!/usr/bin/env python3 # Copyright (c) 2022 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 isfinalxxx RPCS.""" import random from test_framework.address import ADDRESS_ECREG_UNSPENDABLE from test_framework.authproxy import JSONRPCException from test_framework.avatools import AvaP2PInterface from test_framework.blocktools import create_block, create_coinbase from test_framework.messages import ( AvalancheVote, AvalancheVoteError, CBlockHeader, msg_headers, ) from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, assert_raises_rpc_error, uint256_hex, ) QUORUM_NODE_COUNT = 16 class AvalancheIsFinalTest(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 1 self.extra_args = [ [ '-avalanche=1', '-avaproofstakeutxodustthreshold=1000000', '-avaproofstakeutxoconfirmations=1', '-avacooldown=0', '-avaminquorumstake=0', '-avaminavaproofsnodecount=0', ] ] def run_test(self): node = self.nodes[0] tip = node.getbestblockhash() assert_raises_rpc_error( -1, "Avalanche is not ready to poll yet.", self.nodes[0].isfinalblock, tip, ) assert_raises_rpc_error( -1, "Avalanche is not ready to poll yet.", self.nodes[0].isfinaltransaction, node.getblock(tip)['tx'][0], tip, ) # Build a fake quorum of nodes. def get_quorum(): return [node.add_p2p_connection(AvaP2PInterface(node)) for _ in range(0, QUORUM_NODE_COUNT)] - # Pick on node from the quorum for polling. + # Pick one node from the quorum for polling. quorum = get_quorum() def is_quorum_established(): return node.getavalancheinfo()['ready_to_poll'] is True self.wait_until(is_quorum_established) def can_find_block_in_poll( blockhash, 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 == int(blockhash, 16): r = resp found_hash = True votes.append(AvalancheVote(r, inv.hash)) n.send_avaresponse(poll.round, votes, n.delegated_privkey) return found_hash blockhash = node.generate(1)[0] cb_txid = node.getblock(blockhash)['tx'][0] assert not node.isfinalblock(blockhash) assert not node.isfinaltransaction(cb_txid, blockhash) def is_finalblock(blockhash): can_find_block_in_poll(blockhash) return node.isfinalblock(blockhash) with node.assert_debug_log([f"Avalanche finalized block {blockhash}"]): self.wait_until(lambda: is_finalblock(blockhash)) assert node.isfinaltransaction(cb_txid, blockhash) self.log.info("Check block ancestors are finalized as well") tip_height = node.getblockheader(blockhash)['height'] for height in range(0, tip_height): hash = node.getblockhash(height) assert node.isfinalblock(hash) txid = node.getblock(hash)['tx'][0] assert node.isfinaltransaction(txid, hash) if self.is_wallet_compiled(): self.log.info("Check mempool transactions are not finalized") # Mature some utxos tip = node.generate(100)[-1] wallet_txid = node.sendtoaddress( ADDRESS_ECREG_UNSPENDABLE, 1_000_000) assert wallet_txid in node.getrawmempool() assert_raises_rpc_error( -5, "No such transaction found in the provided block.", node.isfinaltransaction, wallet_txid, tip, ) self.log.info( "A transaction is only finalized if the containing block is finalized") tip = node.generate(1)[0] assert wallet_txid not in node.getrawmempool() assert not node.isfinaltransaction(wallet_txid, tip) self.wait_until(lambda: is_finalblock(tip)) assert node.isfinaltransaction(wallet_txid, tip) # Needs -txindex assert_raises_rpc_error( -5, "No such transaction. Use -txindex or provide a block hash to enable blockchain transaction queries.", node.isfinaltransaction, wallet_txid, ) self.log.info( "Repeat with -txindex so we don't need the blockhash") self.restart_node(0, self.extra_args[0] + ['-txindex']) quorum = get_quorum() self.wait_until(is_quorum_established) # Try to raise a -txindex not synced yet error. This is not # guaranteed because syncing is fast! try: node.isfinaltransaction( uint256_hex(random.randint(0, 2**256 - 1)), ) except JSONRPCException as e: assert_equal(e.error['code'], -5) if e.error['message'] == "No such mempool or blockchain transaction.": # If we got a regular "not found" error, the txindex should # have synced. assert node.getindexinfo()['txindex']['synced'] is True else: # Otherwise we might have successfully raised before the # indexer completed. Checking the status now is useless as # the indexer might have completed the synchronization in # the meantime and the status is no longer relevant. assert e.error['message'] == "No such transaction. Blockchain transactions are still in the process of being indexed." else: assert False, "The isfinaltransaction RPC call did not throw as expected." self.wait_until(lambda: node.getindexinfo()[ 'txindex']['synced'] is True) self.wait_until(lambda: is_finalblock(tip)) assert node.isfinaltransaction(wallet_txid) wallet_txid = node.sendtoaddress( ADDRESS_ECREG_UNSPENDABLE, 1_000_000) assert wallet_txid in node.getrawmempool() assert not node.isfinaltransaction(wallet_txid) assert_raises_rpc_error( -5, "No such mempool or blockchain transaction.", node.isfinaltransaction, uint256_hex(random.randint(0, 2**256 - 1)), ) self.log.info("Check unknown item") for _ in range(10): assert_raises_rpc_error( -8, "Block not found", node.isfinalblock, uint256_hex(random.randint(0, 2**256 - 1)), ) assert_raises_rpc_error( -8, "Block not found", node.isfinaltransaction, uint256_hex(random.randint(0, 2**256 - 1)), uint256_hex(random.randint(0, 2**256 - 1)), ) tip = node.getbestblockhash() height = node.getblockcount() + 1 time = node.getblock(tip)['time'] + 1 block = create_block(int(tip, 16), create_coinbase(height), time) block.solve() peer = node.add_p2p_connection(AvaP2PInterface()) msg = msg_headers() msg.headers = [CBlockHeader(block)] peer.send_message(msg) self.wait_until(lambda: node.getchaintips()[0]['height'] == height) assert_raises_rpc_error( -1, "Block data not downloaded yet.", node.isfinaltransaction, uint256_hex(random.randint(0, 2**256 - 1)), uint256_hex(block.sha256), ) if __name__ == '__main__': AvalancheIsFinalTest().main()