diff --git a/test/functional/abc_p2p_avalanche_peer_discovery.py b/test/functional/abc_p2p_avalanche_peer_discovery.py index 3727957fc..fe478170d 100755 --- a/test/functional/abc_p2p_avalanche_peer_discovery.py +++ b/test/functional/abc_p2p_avalanche_peer_discovery.py @@ -1,209 +1,209 @@ #!/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 peer discovery behavior of avalanche nodes. This includes tests for the service flag, avahello handshake and proof exchange. """ import time from test_framework.avatools import ( get_ava_p2p_interface, create_coinbase_stakes, get_proof_ids, ) from test_framework.key import ( bytes_to_wif, ECKey, ECPubKey, ) from test_framework.p2p import p2p_lock from test_framework.messages import ( AvalancheProof, CInv, FromHex, MSG_AVA_PROOF, msg_getdata, NODE_AVALANCHE, NODE_NETWORK, ) from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, wait_until, ) UNCONDITIONAL_RELAY_DELAY = 2 * 60 class AvalancheTest(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 1 self.extra_args = [['-enableavalanche=1']] self.supports_cli = False def run_test(self): node = self.nodes[0] # duplicate the deterministic sig test from src/test/key_tests.cpp privkey = ECKey() privkey.set(bytes.fromhex( "12b004fff7f4b69ef8650e767f18f11ede158148b425660723b9f9a66e61f747"), True) pubkey = privkey.get_pubkey() self.log.info( "Check the node is signalling the avalanche service bit only if there is a proof.") assert_equal( int(node.getnetworkinfo()['localservices'], 16) & NODE_AVALANCHE, 0) # Create stakes by mining blocks addrkey0 = node.get_deterministic_priv_key() blockhashes = node.generatetoaddress(2, addrkey0.address) stakes = create_coinbase_stakes(node, [blockhashes[0]], addrkey0.key) proof_sequence = 11 proof_expiration = 12 proof = node.buildavalancheproof( proof_sequence, proof_expiration, pubkey.get_bytes().hex(), stakes) # Restart the node self.restart_node(0, self.extra_args[0] + [ "-avaproof={}".format(proof), "-avamasterkey=cND2ZvtabDbJ1gucx9GWH6XT9kgTAqfb6cotPt5Q5CyxVDhid2EN", ]) assert_equal( int(node.getnetworkinfo()['localservices'], 16) & NODE_AVALANCHE, NODE_AVALANCHE) def check_avahello(args): # Restart the node with the given args self.restart_node(0, self.extra_args[0] + args) peer = get_ava_p2p_interface(node) avahello = peer.wait_for_avahello().hello avakey = ECPubKey() avakey.set(bytes.fromhex(node.getavalanchekey())) assert avakey.verify_schnorr( avahello.sig, avahello.get_sighash(peer)) self.log.info( "Test the avahello signature with a generated delegation") check_avahello([ "-avaproof={}".format(proof), "-avamasterkey=cND2ZvtabDbJ1gucx9GWH6XT9kgTAqfb6cotPt5Q5CyxVDhid2EN" ]) master_key = ECKey() master_key.generate() limited_id = FromHex(AvalancheProof(), proof).limited_proofid delegation = node.delegateavalancheproof( f"{limited_id:0{64}x}", bytes_to_wif(privkey.get_bytes()), master_key.get_pubkey().get_bytes().hex(), ) self.log.info("Test the avahello signature with a supplied delegation") check_avahello([ "-avaproof={}".format(proof), "-avadelegation={}".format(delegation), "-avamasterkey={}".format(bytes_to_wif(master_key.get_bytes())), ]) stakes = create_coinbase_stakes(node, [blockhashes[1]], addrkey0.key) interface_proof_hex = node.buildavalancheproof( proof_sequence, proof_expiration, pubkey.get_bytes().hex(), stakes) limited_id = FromHex( AvalancheProof(), interface_proof_hex).limited_proofid # delegate delegated_key = ECKey() delegated_key.generate() interface_delegation_hex = node.delegateavalancheproof( f"{limited_id:0{64}x}", bytes_to_wif(privkey.get_bytes()), delegated_key.get_pubkey().get_bytes().hex(), None) self.log.info("Test that wrong avahello signature causes a ban") bad_interface = get_ava_p2p_interface(node) wrong_key = ECKey() wrong_key.generate() - with self.nodes[0].assert_debug_log( + with node.assert_debug_log( ["Misbehaving", "peer=1 (0 -> 100) BAN THRESHOLD EXCEEDED: invalid-avahello-signature"]): bad_interface.send_avahello(interface_delegation_hex, wrong_key) bad_interface.wait_for_disconnect() self.log.info( 'Check that receiving a valid avahello triggers a proof getdata request') good_interface = get_ava_p2p_interface(node) proofid = good_interface.send_avahello( interface_delegation_hex, delegated_key) def getdata_found(): with p2p_lock: return good_interface.last_message.get( "getdata") and good_interface.last_message["getdata"].inv[-1].hash == proofid wait_until(getdata_found) self.log.info('Check that we can download the proof from our peer') node_proofid = FromHex(AvalancheProof(), proof).proofid def wait_for_proof_validation(): # Connect some blocks to trigger the proof verification node.generate(1) wait_until(lambda: node_proofid in get_proof_ids(node)) wait_for_proof_validation() getdata = msg_getdata([CInv(MSG_AVA_PROOF, node_proofid)]) self.log.info( "Proof has been inv'ed recently, check it can be requested") good_interface.send_message(getdata) def proof_received(peer): with p2p_lock: return peer.last_message.get( "avaproof") and peer.last_message["avaproof"].proof.proofid == node_proofid wait_until(lambda: proof_received(good_interface)) # Restart the node self.restart_node(0, self.extra_args[0] + [ "-avaproof={}".format(proof), "-avamasterkey=cND2ZvtabDbJ1gucx9GWH6XT9kgTAqfb6cotPt5Q5CyxVDhid2EN", ]) wait_for_proof_validation() self.log.info( "The proof has not been announced, it cannot be requested") peer = get_ava_p2p_interface(node, services=NODE_NETWORK) peer.send_message(getdata) # Give enough time for the node to answer. Since we cannot check for a # non-event this is the best we can do time.sleep(2) assert not proof_received(peer) self.log.info("The proof is known for long enough to be requested") current_time = int(time.time()) node.setmocktime(current_time + UNCONDITIONAL_RELAY_DELAY) peer.send_message(getdata) wait_until(lambda: proof_received(peer)) if __name__ == '__main__': AvalancheTest().main() diff --git a/test/functional/abc_p2p_avalanche_voting.py b/test/functional/abc_p2p_avalanche_voting.py index 17d779960..a77c86523 100755 --- a/test/functional/abc_p2p_avalanche_voting.py +++ b/test/functional/abc_p2p_avalanche_voting.py @@ -1,253 +1,253 @@ #!/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, create_coinbase_stakes, ) from test_framework.key import ( ECKey, ECPubKey, ) from test_framework.messages import AvalancheVote from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, wait_until, ) BLOCK_ACCEPTED = 0 BLOCK_INVALID = 1 BLOCK_PARKED = 2 BLOCK_FORK = 3 BLOCK_UNKNOWN = -1 BLOCK_MISSING = -2 BLOCK_PENDING = -3 QUORUM_NODE_COUNT = 16 class AvalancheTest(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 2 self.extra_args = [ ['-enableavalanche=1', '-avacooldown=0'], ['-enableavalanche=1', '-avacooldown=0', '-noparkdeepreorg', '-maxreorgdepth=-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. quorum = get_quorum() poll_node = quorum[0] # Generate many block and poll for them. addrkey0 = node.get_deterministic_priv_key() blockhashes = node.generatetoaddress(100, addrkey0.address) # Use the first coinbase to create a stake stakes = create_coinbase_stakes(node, [blockhashes[0]], addrkey0.key) 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(BLOCK_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(BLOCK_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(BLOCK_ACCEPTED, h) for h in various_block_hashes[:5]] + [AvalancheVote(BLOCK_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(BLOCK_ACCEPTED, h) for h in various_block_hashes[:3]] + [AvalancheVote(BLOCK_FORK, h) for h in various_block_hashes[3:6]] + [AvalancheVote(BLOCK_UNKNOWN, h) for h in various_block_hashes[-3:]]) self.log.info("Trigger polling from the node...") # duplicate the deterministic sig test from src/test/key_tests.cpp privkey = ECKey() privkey.set(bytes.fromhex( "12b004fff7f4b69ef8650e767f18f11ede158148b425660723b9f9a66e61f747"), True) pubkey = privkey.get_pubkey() proof_sequence = 11 proof_expiration = 12 proof = node.buildavalancheproof( proof_sequence, proof_expiration, pubkey.get_bytes().hex(), stakes) # Activate the quorum. for n in quorum: success = node.addavalanchenode( n.nodeid, pubkey.get_bytes().hex(), proof) assert success is True self.log.info("Testing getavalanchepeerinfo...") avapeerinfo = node.getavalanchepeerinfo() # There is a single peer because all nodes share the same proof. assert_equal(len(avapeerinfo), 1) assert_equal(avapeerinfo[0]["peerid"], 0) assert_equal(avapeerinfo[0]["nodecount"], len(quorum)) # The first avalanche node index is 1, because 0 is self.nodes[1]. assert_equal(sorted(avapeerinfo[0]["nodes"]), list(range(1, QUORUM_NODE_COUNT + 1))) assert_equal(avapeerinfo[0]["proof"], proof) def can_find_block_in_poll(hash, resp=BLOCK_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 = BLOCK_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 # Now that we have a peer, we should start polling for the tip. hash_tip = int(node.getbestblockhash(), 16) 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() 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. 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, BLOCK_PARKED) return node.getbestblockhash() == fork_tip # Because everybody answers no, the node will park that block. 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 self.nodes[0].assert_debug_log( + 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=privkey) if __name__ == '__main__': AvalancheTest().main() diff --git a/test/functional/abc_p2p_proof_inventory.py b/test/functional/abc_p2p_proof_inventory.py index 541f28b51..66a2bec89 100644 --- a/test/functional/abc_p2p_proof_inventory.py +++ b/test/functional/abc_p2p_proof_inventory.py @@ -1,300 +1,300 @@ #!/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 proof inventory relaying """ from test_framework.avatools import ( create_coinbase_stakes, get_proof_ids, wait_for_proof, ) from test_framework.address import ADDRESS_BCHREG_UNSPENDABLE from test_framework.key import ECKey, bytes_to_wif from test_framework.messages import ( AvalancheProof, CInv, FromHex, MSG_AVA_PROOF, MSG_TYPE_MASK, msg_avaproof, msg_getdata, ) from test_framework.p2p import ( P2PInterface, p2p_lock, ) from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, assert_greater_than, connect_nodes, wait_until, ) import time # Broadcast reattempt occurs every 10 to 15 minutes MAX_INITIAL_BROADCAST_DELAY = 15 * 60 # Delay to allow the node to respond to getdata requests UNCONDITIONAL_RELAY_DELAY = 2 * 60 class ProofInvStoreP2PInterface(P2PInterface): def __init__(self): super().__init__() self.proof_invs_counter = 0 def on_inv(self, message): for i in message.inv: if i.type & MSG_TYPE_MASK == MSG_AVA_PROOF: self.proof_invs_counter += 1 class ProofInventoryTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 5 self.extra_args = [['-enableavalanche=1', '-avacooldown=0']] * self.num_nodes def gen_proof(self, node): blockhashes = node.generate(10) privkey = ECKey() privkey.generate() pubkey = privkey.get_pubkey() stakes = create_coinbase_stakes( node, blockhashes, node.get_deterministic_priv_key().key) proof_hex = node.buildavalancheproof( 42, 2000000000, pubkey.get_bytes().hex(), stakes) return bytes_to_wif(privkey.get_bytes()), FromHex( AvalancheProof(), proof_hex) def test_send_proof_inv(self): self.log.info("Test sending a proof to our peers") node = self.nodes[0] for i in range(10): node.add_p2p_connection(ProofInvStoreP2PInterface()) _, proof = self.gen_proof(node) assert node.sendavalancheproof(proof.serialize().hex()) def proof_inv_found(peer): with p2p_lock: return peer.last_message.get( "inv") and peer.last_message["inv"].inv[-1].hash == proof.proofid wait_until(lambda: all(proof_inv_found(i) for i in node.p2ps)) self.log.info("Test that we don't send the same inv several times") extra_peer = ProofInvStoreP2PInterface() node.add_p2p_connection(extra_peer) # Send the same proof one more time node.sendavalancheproof(proof.serialize().hex()) # Our new extra peer should receive it but not the others wait_until(lambda: proof_inv_found(extra_peer)) assert all(p.proof_invs_counter == 1 for p in node.p2ps) # Send the proof again and force the send loop to be processed for peer in node.p2ps: node.sendavalancheproof(proof.serialize().hex()) peer.sync_with_ping() assert all(p.proof_invs_counter == 1 for p in node.p2ps) def test_receive_proof(self): self.log.info("Test a peer is created on proof reception") node = self.nodes[0] _, proof = self.gen_proof(node) peer = node.add_p2p_connection(P2PInterface()) msg = msg_avaproof() msg.proof = proof peer.send_message(msg) wait_until(lambda: proof.proofid in get_proof_ids(node)) self.log.info("Test receiving a proof with missing utxo is orphaned") privkey = ECKey() privkey.generate() orphan_hex = node.buildavalancheproof( 42, 2000000000, privkey.get_pubkey().get_bytes().hex(), [{ 'txid': '0' * 64, 'vout': 0, 'amount': 10e6, 'height': 42, 'iscoinbase': False, 'privatekey': bytes_to_wif(privkey.get_bytes()), }] ) orphan = FromHex(AvalancheProof(), orphan_hex) orphan_proofid = "{:064x}".format(orphan.proofid) msg = msg_avaproof() msg.proof = orphan peer.send_message(msg) wait_for_proof(node, orphan_proofid, expect_orphan=True) def test_ban_invalid_proof(self): node = self.nodes[0] _, bad_proof = self.gen_proof(node) bad_proof.stakes = [] peer = node.add_p2p_connection(P2PInterface()) msg = msg_avaproof() msg.proof = bad_proof with node.assert_debug_log([ 'Misbehaving', 'invalid-avaproof', ]): peer.send_message(msg) peer.wait_for_disconnect() def test_proof_relay(self): # This test makes no sense with a single node ! assert_greater_than(self.num_nodes, 1) def restart_nodes_with_proof(nodes=self.nodes): proofids = set() for i, node in enumerate(nodes): privkey, proof = self.gen_proof(node) proofids.add(proof.proofid) self.restart_node(node.index, self.extra_args[node.index] + [ "-avaproof={}".format(proof.serialize().hex()), "-avamasterkey={}".format(privkey) ]) # Connect a block to make the proof be added to our pool node.generate(1) wait_until(lambda: proof.proofid in get_proof_ids(node)) [connect_nodes(node, n) for n in nodes[:i]] return proofids proofids = restart_nodes_with_proof(self.nodes) self.log.info("Nodes should eventually get the proof from their peer") self.sync_proofs() for node in self.nodes: assert_equal(set(get_proof_ids(node)), proofids) def test_unbroadcast(self): self.log.info("Test broadcasting proofs") node = self.nodes[0] def add_peers(count): peers = [] for i in range(count): peer = node.add_p2p_connection(ProofInvStoreP2PInterface()) peer.wait_for_verack() peers.append(peer) return peers _, proof = self.gen_proof(node) proofid_hex = "{:064x}".format(proof.proofid) # Broadcast the proof peers = add_peers(3) assert node.sendavalancheproof(proof.serialize().hex()) wait_for_proof(node, proofid_hex) def proof_inv_received(peers): with p2p_lock: return all(p.last_message.get( "inv") and p.last_message["inv"].inv[-1].hash == proof.proofid for p in peers) wait_until(lambda: proof_inv_received(peers)) # If no peer request the proof for download, the node should reattempt # broadcasting to all new peers after 10 to 15 minutes. peers = add_peers(3) node.mockscheduler(MAX_INITIAL_BROADCAST_DELAY + 1) peers[-1].sync_with_ping() wait_until(lambda: proof_inv_received(peers)) # If at least one peer requests the proof, there is no more attempt to # broadcast it node.setmocktime(int(time.time()) + UNCONDITIONAL_RELAY_DELAY) msg = msg_getdata([CInv(t=MSG_AVA_PROOF, h=proof.proofid)]) peers[-1].send_message(msg) # Give enough time for the node to broadcast the proof again peers = add_peers(3) node.mockscheduler(MAX_INITIAL_BROADCAST_DELAY + 1) peers[-1].sync_with_ping() assert not proof_inv_received(peers) self.log.info( "Proofs that become invalid should no longer be broadcasted") # Restart and add connect a new set of peers self.restart_node(0) # Broadcast the proof peers = add_peers(3) assert node.sendavalancheproof(proof.serialize().hex()) 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) # Mature the utxo then spend it node.generate(100) utxo = proof.stakes[0].stake.utxo raw_tx = node.createrawtransaction( inputs=[{ # coinbase "txid": "{:064x}".format(utxo.hash), "vout": utxo.n }], outputs={ADDRESS_BCHREG_UNSPENDABLE: 25_000_000 - 250.00}, ) - signed_tx = self.nodes[0].signrawtransactionwithkey( + signed_tx = node.signrawtransactionwithkey( hexstring=raw_tx, privkeys=[node.get_deterministic_priv_key().key], ) node.sendrawtransaction(signed_tx['hex']) # Mine the tx in a block node.generate(1) # Wait for the proof to be orphaned wait_until(lambda: node.getrawavalancheproof( proofid_hex)["orphan"] is True) # It should no longer be broadcasted peers = add_peers(3) node.mockscheduler(MAX_INITIAL_BROADCAST_DELAY + 1) peers[-1].sync_with_ping() assert not proof_inv_received(peers) def run_test(self): self.test_send_proof_inv() self.test_receive_proof() self.test_ban_invalid_proof() self.test_proof_relay() self.test_unbroadcast() if __name__ == '__main__': ProofInventoryTest().main()