diff --git a/test/functional/abc_p2p_avalanche_peer_discovery.py b/test/functional/abc_p2p_avalanche_peer_discovery.py index 4200d6793..3727957fc 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( ["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(2) + 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_rpc_avalancheproof.py b/test/functional/abc_rpc_avalancheproof.py index 872443675..06880aa9c 100644 --- a/test/functional/abc_rpc_avalancheproof.py +++ b/test/functional/abc_rpc_avalancheproof.py @@ -1,433 +1,433 @@ #!/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 building avalanche proofs and using them to add avalanche peers.""" import base64 from decimal import Decimal from test_framework.address import ADDRESS_BCHREG_UNSPENDABLE from test_framework.avatools import ( create_coinbase_stakes, create_stakes, get_proof_ids, ) from test_framework.key import ECKey, bytes_to_wif from test_framework.messages import ( AvalancheDelegation, AvalancheDelegationLevel, AvalancheProof, FromHex, ) from test_framework.p2p import P2PInterface, p2p_lock from test_framework.test_framework import BitcoinTestFramework from test_framework.test_node import ErrorMatch from test_framework.util import ( append_config, assert_equal, wait_until, assert_raises_rpc_error, ) AVALANCHE_MAX_PROOF_STAKES = 1000 PROOF_DUST_THRESHOLD = 1000000.0 """Minimum amount per UTXO in a proof (in coins, not in satoshis)""" def add_interface_node(test_node) -> str: """Create a mininode, connect it to test_node, return the nodeid of the mininode as registered by test_node. """ n = P2PInterface() test_node.add_p2p_connection(n) n.wait_for_verack() return test_node.getpeerinfo()[-1]['id'] class AvalancheProofTest(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 1 self.extra_args = [['-enableavalanche=1', '-avacooldown=0'], ] self.supports_cli = False self.rpc_timeout = 120 def run_test(self): node = self.nodes[0] addrkey0 = node.get_deterministic_priv_key() blockhashes = node.generatetoaddress(100, addrkey0.address) self.log.info( "Make build a valid proof and restart the node to use it") privkey = ECKey() privkey.set(bytes.fromhex( "12b004fff7f4b69ef8650e767f18f11ede158148b425660723b9f9a66e61f747"), True) def get_hex_pubkey(privkey): return privkey.get_pubkey().get_bytes().hex() proof_master = get_hex_pubkey(privkey) proof_sequence = 11 proof_expiration = 12 stakes = create_coinbase_stakes(node, [blockhashes[0]], addrkey0.key) proof = node.buildavalancheproof( proof_sequence, proof_expiration, proof_master, stakes) self.log.info("Test decodeavalancheproof RPC") proofobj = FromHex(AvalancheProof(), proof) decodedproof = node.decodeavalancheproof(proof) limited_id_hex = f"{proofobj.limited_proofid:0{64}x}" assert_equal(decodedproof["sequence"], proof_sequence) assert_equal(decodedproof["expiration"], proof_expiration) assert_equal(decodedproof["master"], proof_master) assert_equal(decodedproof["proofid"], f"{proofobj.proofid:0{64}x}") assert_equal(decodedproof["limitedid"], limited_id_hex) assert_equal(decodedproof["stakes"][0]["txid"], stakes[0]["txid"]) assert_equal(decodedproof["stakes"][0]["vout"], stakes[0]["vout"]) assert_equal(decodedproof["stakes"][0]["height"], stakes[0]["height"]) assert_equal( decodedproof["stakes"][0]["iscoinbase"], stakes[0]["iscoinbase"]) assert_equal( decodedproof["stakes"][0]["signature"], base64.b64encode(proofobj.stakes[0].sig).decode("ascii")) # Invalid hex (odd number of hex digits) assert_raises_rpc_error(-22, "Proof must be an hexadecimal string", node.decodeavalancheproof, proof[:-1]) # Valid hex but invalid proof assert_raises_rpc_error(-22, "Proof has invalid format", node.decodeavalancheproof, proof[:-2]) # Restart the node, making sure it is initially in IBD mode minchainwork = int(node.getblockchaininfo()["chainwork"], 16) + 1 self.restart_node(0, self.extra_args[0] + [ "-avaproof={}".format(proof), "-avamasterkey=cND2ZvtabDbJ1gucx9GWH6XT9kgTAqfb6cotPt5Q5CyxVDhid2EN", "-minimumchainwork=0x{:x}".format(minchainwork), ]) self.log.info( "The proof verification should be delayed until IBD is complete") assert node.getblockchaininfo()["initialblockdownload"] is True # Our proof cannot be verified during IBD, so we should have no peer assert not node.getavalanchepeerinfo() - # Mining a few more blocks should cause us to leave IBD - node.generate(2) + # Mining one more block should cause us to leave IBD + node.generate(1) # Our proof is now verified and our node is added as a peer assert node.getblockchaininfo()["initialblockdownload"] is False wait_until(lambda: len(node.getavalanchepeerinfo()) == 1, timeout=5) if self.is_wallet_compiled(): self.log.info( "A proof using the maximum number of stakes is accepted...") new_blocks = node.generate(AVALANCHE_MAX_PROOF_STAKES // 10 + 1) # confirm the coinbase UTXOs node.generate(101) too_many_stakes = create_stakes( node, new_blocks, AVALANCHE_MAX_PROOF_STAKES + 1) maximum_stakes = too_many_stakes[:-1] good_proof = node.buildavalancheproof( proof_sequence, proof_expiration, proof_master, maximum_stakes) peerid1 = add_interface_node(node) assert node.addavalanchenode(peerid1, proof_master, good_proof) self.log.info( "A proof using too many stakes should be rejected...") too_many_utxos = node.buildavalancheproof( proof_sequence, proof_expiration, proof_master, too_many_stakes) peerid2 = add_interface_node(node) assert not node.addavalanchenode( peerid2, proof_master, too_many_utxos) self.log.info("Generate delegations for the proof") # Stack up a few delegation levels def gen_privkey(): pk = ECKey() pk.generate() return pk delegator_privkey = privkey delegation = None for _ in range(10): delegated_privkey = gen_privkey() delegation = node.delegateavalancheproof( limited_id_hex, bytes_to_wif(delegator_privkey.get_bytes()), get_hex_pubkey(delegated_privkey), delegation, ) delegator_privkey = delegated_privkey random_privkey = gen_privkey() random_pubkey = get_hex_pubkey(random_privkey) # Invalid proof no_stake = node.buildavalancheproof(proof_sequence, proof_expiration, proof_master, []) # Invalid privkey assert_raises_rpc_error(-5, "The private key is invalid", node.delegateavalancheproof, limited_id_hex, bytes_to_wif(bytes(32)), random_pubkey, ) # Invalid delegation bad_dg = AvalancheDelegation() assert_raises_rpc_error(-8, "The supplied delegation does not match the proof", node.delegateavalancheproof, limited_id_hex, bytes_to_wif(privkey.get_bytes()), random_pubkey, bad_dg.serialize().hex(), ) # Still invalid, but with a matching proofid bad_dg.limited_proofid = proofobj.limited_proofid bad_dg.proof_master = proofobj.master bad_dg.levels = [AvalancheDelegationLevel()] assert_raises_rpc_error(-8, "The supplied delegation is not valid", node.delegateavalancheproof, limited_id_hex, bytes_to_wif(privkey.get_bytes()), random_pubkey, bad_dg.serialize().hex(), ) # Wrong privkey, match the proof but does not match the delegation assert_raises_rpc_error(-8, "The supplied private key does not match the delegation", node.delegateavalancheproof, limited_id_hex, bytes_to_wif(privkey.get_bytes()), random_pubkey, delegation, ) # Delegation not hex assert_raises_rpc_error(-22, "Delegation must be an hexadecimal string.", node.delegateavalancheproof, limited_id_hex, bytes_to_wif(privkey.get_bytes()), random_pubkey, "f00", ) # Delegation is hex but ill-formed assert_raises_rpc_error(-22, "Delegation has invalid format", node.delegateavalancheproof, limited_id_hex, bytes_to_wif(privkey.get_bytes()), random_pubkey, "dead", ) # Test invalid proofs dust = node.buildavalancheproof( proof_sequence, proof_expiration, proof_master, create_coinbase_stakes(node, [blockhashes[0]], addrkey0.key, amount="0")) dust_amount = Decimal(f"{PROOF_DUST_THRESHOLD * 0.9999:.4f}") dust2 = node.buildavalancheproof( proof_sequence, proof_expiration, proof_master, create_coinbase_stakes(node, [blockhashes[0]], addrkey0.key, amount=str(dust_amount))) duplicate_stake = node.buildavalancheproof( proof_sequence, proof_expiration, proof_master, create_coinbase_stakes(node, [blockhashes[0]] * 2, addrkey0.key)) missing_stake = node.buildavalancheproof( proof_sequence, proof_expiration, proof_master, [{ 'txid': '0' * 64, 'vout': 0, 'amount': 10000000, 'height': 42, 'iscoinbase': False, 'privatekey': addrkey0.key, }] ) bad_sig = ("0b000000000000000c0000000000000021030b4c866585dd868a9d62348" "a9cd008d6a312937048fff31670e7e920cfc7a7440105c5f72f5d6da3085" "583e75ee79340eb4eff208c89988e7ed0efb30b87298fa30000000000f20" "52a0100000003000000210227d85ba011276cf25b51df6a188b75e604b3" "8770a462b2d0e9fb2fc839ef5d3faf07f001dd38e9b4a43d07d5d449cc0" "f7d2888d96b82962b3ce516d1083c0e031773487fc3c4f2e38acd1db974" "1321b91a79b82d1c2cfd47793261e4ba003cf5") self.log.info( "Check the verifyavalancheproof and sendavalancheproof RPCs") for rpc in [node.verifyavalancheproof, node.sendavalancheproof]: assert_raises_rpc_error(-22, "Proof must be an hexadecimal string", rpc, "f00") assert_raises_rpc_error(-22, "Proof has invalid format", rpc, "f00d") def check_rpc_failure(proof, message): assert_raises_rpc_error(-8, "The proof is invalid: " + message, rpc, proof) check_rpc_failure(no_stake, "no-stake") check_rpc_failure(dust, "amount-below-dust-threshold") check_rpc_failure(duplicate_stake, "duplicated-stake") check_rpc_failure(missing_stake, "utxo-missing-or-spent") check_rpc_failure(bad_sig, "invalid-signature") if self.is_wallet_compiled(): check_rpc_failure(too_many_utxos, "too-many-utxos") conflicting_utxo = node.buildavalancheproof( proof_sequence + 1, proof_expiration, proof_master, stakes) assert_raises_rpc_error(-8, "The proof has conflicting utxo with an existing proof", node.sendavalancheproof, conflicting_utxo) # Good proof assert node.verifyavalancheproof(proof) peer = node.add_p2p_connection(P2PInterface()) proofid = FromHex(AvalancheProof(), proof).proofid node.sendavalancheproof(proof) assert proofid in get_proof_ids(node) def inv_found(): with p2p_lock: return peer.last_message.get( "inv") and peer.last_message["inv"].inv[-1].hash == proofid wait_until(inv_found) self.log.info("Check the getrawproof RPC") raw_proof = node.getrawavalancheproof("{:064x}".format(proofid)) assert_equal(raw_proof['proof'], proof) assert_equal(raw_proof['orphan'], False) assert_raises_rpc_error(-8, "Proof not found", node.getrawavalancheproof, '0' * 64) # Orphan the proof by sending the stake raw_tx = node.createrawtransaction( [{"txid": stakes[-1]["txid"], "vout": 0}], {ADDRESS_BCHREG_UNSPENDABLE: stakes[-1] ["amount"] - Decimal('10000')} ) signed_tx = node.signrawtransactionwithkey(raw_tx, [addrkey0.key]) node.sendrawtransaction(signed_tx["hex"]) node.generate(1) wait_until(lambda: proofid not in get_proof_ids(node)) raw_proof = node.getrawavalancheproof("{:064x}".format(proofid)) assert_equal(raw_proof['proof'], proof) assert_equal(raw_proof['orphan'], True) self.log.info("Bad proof should be rejected at startup") self.stop_node(0) node.assert_start_raises_init_error( self.extra_args[0] + [ "-avasessionkey=0", ], expected_msg="Error: The avalanche session key is invalid.", ) node.assert_start_raises_init_error( self.extra_args[0] + [ "-avaproof={}".format(proof), ], expected_msg="Error: The avalanche master key is missing for the avalanche proof.", ) node.assert_start_raises_init_error( self.extra_args[0] + [ "-avaproof={}".format(proof), "-avamasterkey=0", ], expected_msg="Error: The avalanche master key is invalid.", ) def check_proof_init_error(proof, message): node.assert_start_raises_init_error( self.extra_args[0] + [ "-avaproof={}".format(proof), "-avamasterkey=cND2ZvtabDbJ1gucx9GWH6XT9kgTAqfb6cotPt5Q5CyxVDhid2EN", ], expected_msg="Error: " + message, ) check_proof_init_error(no_stake, "The avalanche proof has no stake.") check_proof_init_error(dust, "The avalanche proof stake is too low.") check_proof_init_error(dust2, "The avalanche proof stake is too low.") check_proof_init_error(duplicate_stake, "The avalanche proof has duplicated stake.") check_proof_init_error(bad_sig, "The avalanche proof has invalid stake signatures.") if self.is_wallet_compiled(): # The too many utxos case creates a proof which is that large that it # cannot fit on the command line append_config(node.datadir, ["avaproof={}".format(too_many_utxos)]) node.assert_start_raises_init_error( self.extra_args[0] + [ "-avamasterkey=cND2ZvtabDbJ1gucx9GWH6XT9kgTAqfb6cotPt5Q5CyxVDhid2EN", ], expected_msg="Error: The avalanche proof has too many utxos.", match=ErrorMatch.PARTIAL_REGEX, ) # Master private key mismatch random_privkey = ECKey() random_privkey.generate() node.assert_start_raises_init_error( self.extra_args[0] + [ "-avaproof={}".format(proof), "-avamasterkey={}".format( bytes_to_wif(random_privkey.get_bytes())), ], expected_msg="Error: The master key does not match the proof public key.", ) self.log.info("Bad delegation should be rejected at startup") def check_delegation_init_error(delegation, message): node.assert_start_raises_init_error( self.extra_args[0] + [ "-avadelegation={}".format(delegation), "-avaproof={}".format(proof), "-avamasterkey={}".format( bytes_to_wif(delegated_privkey.get_bytes())), ], expected_msg="Error: " + message, ) check_delegation_init_error( AvalancheDelegation().serialize().hex(), "The delegation does not match the proof.") bad_level_sig = FromHex(AvalancheDelegation(), delegation) # Tweak some key to cause the signature to mismatch bad_level_sig.levels[-2].pubkey = bytes.fromhex(proof_master) check_delegation_init_error(bad_level_sig.serialize().hex(), "The avalanche delegation has invalid signatures.") node.assert_start_raises_init_error( self.extra_args[0] + [ "-avadelegation={}".format(delegation), "-avaproof={}".format(proof), "-avamasterkey={}".format( bytes_to_wif(random_privkey.get_bytes())), ], expected_msg="Error: The master key does not match the delegation public key.", ) if __name__ == '__main__': AvalancheProofTest().main() diff --git a/test/functional/p2p_inv_download.py b/test/functional/p2p_inv_download.py index 739d834f7..42a7b474a 100755 --- a/test/functional/p2p_inv_download.py +++ b/test/functional/p2p_inv_download.py @@ -1,480 +1,480 @@ #!/usr/bin/env python3 # Copyright (c) 2019 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """ Test inventory download behavior """ from test_framework.address import ADDRESS_BCHREG_UNSPENDABLE from test_framework.avatools import wait_for_proof from test_framework.key import ECKey, bytes_to_wif from test_framework.messages import ( AvalancheProof, CInv, CTransaction, FromHex, MSG_AVA_PROOF, MSG_TX, MSG_TYPE_MASK, msg_avaproof, msg_inv, msg_notfound, ) from test_framework.p2p import ( P2PInterface, p2p_lock, ) from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, assert_raises_rpc_error, wait_until, ) import functools import time class TestP2PConn(P2PInterface): def __init__(self, inv_type): super().__init__() self.inv_type = inv_type self.getdata_count = 0 def on_getdata(self, message): for i in message.inv: if i.type & MSG_TYPE_MASK == self.inv_type: self.getdata_count += 1 class NetConstants: """Constants from net_processing""" def __init__(self, getdata_interval, inbound_peer_delay, overloaded_peer_delay, max_getdata_in_flight, max_peer_announcements, bypass_request_limits_permission_flags, ): self.getdata_interval = getdata_interval self.inbound_peer_delay = inbound_peer_delay self.overloaded_peer_delay = overloaded_peer_delay self.max_getdata_in_flight = max_getdata_in_flight self.max_peer_announcements = max_peer_announcements self.max_getdata_inbound_wait = self.getdata_interval + self.inbound_peer_delay self.bypass_request_limits_permission_flags = bypass_request_limits_permission_flags class TestContext: def __init__(self, inv_type, inv_name, constants): self.inv_type = inv_type self.inv_name = inv_name self.constants = constants def p2p_conn(self): return TestP2PConn(self.inv_type) PROOF_TEST_CONTEXT = TestContext( MSG_AVA_PROOF, "avalanche proof", NetConstants( getdata_interval=60, # seconds inbound_peer_delay=2, # seconds overloaded_peer_delay=2, # seconds max_getdata_in_flight=100, max_peer_announcements=5000, bypass_request_limits_permission_flags="bypass_proof_request_limits", ), ) TX_TEST_CONTEXT = TestContext( MSG_TX, "transaction", NetConstants( getdata_interval=60, # seconds inbound_peer_delay=2, # seconds overloaded_peer_delay=2, # seconds max_getdata_in_flight=100, max_peer_announcements=5000, bypass_request_limits_permission_flags="relay", ), ) # Python test constants NUM_INBOUND = 10 def skip(context): def decorator(test): @functools.wraps(test) def wrapper(*args, **kwargs): # Assume the signature is test(self, context) unless context is # passed by name call_context = kwargs.get("context", args[1]) if call_context == context: return lambda *args, **kwargs: None return test(*args, **kwargs) return wrapper return decorator class InventoryDownloadTest(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = False self.num_nodes = 2 self.extra_args = [['-enableavalanche=1', '-avacooldown=0']] * self.num_nodes def test_data_requests(self, context): self.log.info( "Test that we request data from all our peers, eventually") invid = 0xdeadbeef self.log.info("Announce the invid from each incoming peer to node 0") msg = msg_inv([CInv(t=context.inv_type, h=invid)]) for p in self.nodes[0].p2ps: p.send_and_ping(msg) outstanding_peer_index = [i for i in range(len(self.nodes[0].p2ps))] def getdata_found(peer_index): p = self.nodes[0].p2ps[peer_index] with p2p_lock: return p.last_message.get( "getdata") and p.last_message["getdata"].inv[-1].hash == invid node_0_mocktime = int(time.time()) while outstanding_peer_index: node_0_mocktime += context.constants.max_getdata_inbound_wait self.nodes[0].setmocktime(node_0_mocktime) wait_until(lambda: any(getdata_found(i) for i in outstanding_peer_index)) for i in outstanding_peer_index: if getdata_found(i): outstanding_peer_index.remove(i) self.nodes[0].setmocktime(0) self.log.info("All outstanding peers received a getdata") @skip(PROOF_TEST_CONTEXT) def test_inv_block(self, context): self.log.info("Generate a transaction on node 0") tx = self.nodes[0].createrawtransaction( inputs=[{ # coinbase "txid": self.nodes[0].getblock(self.nodes[0].getblockhash(1))['tx'][0], "vout": 0 }], outputs={ADDRESS_BCHREG_UNSPENDABLE: 50000000 - 250.00}, ) tx = self.nodes[0].signrawtransactionwithkey( hexstring=tx, privkeys=[self.nodes[0].get_deterministic_priv_key().key], )['hex'] ctx = FromHex(CTransaction(), tx) txid = int(ctx.rehash(), 16) self.log.info( "Announce the transaction to all nodes from all {} incoming peers, but never send it".format(NUM_INBOUND)) msg = msg_inv([CInv(t=context.inv_type, h=txid)]) for p in self.peers: p.send_and_ping(msg) self.log.info("Put the tx in node 0's mempool") self.nodes[0].sendrawtransaction(tx) # Since node 1 is connected outbound to an honest peer (node 0), it # should get the tx within a timeout. (Assuming that node 0 # announced the tx within the timeout) # The timeout is the sum of # * the worst case until the tx is first requested from an inbound # peer, plus # * the first time it is re-requested from the outbound peer, plus # * 2 seconds to avoid races assert self.nodes[1].getpeerinfo()[0]['inbound'] is False timeout = 2 + context.constants.inbound_peer_delay + \ context.constants.getdata_interval self.log.info( "Tx should be received at node 1 after {} seconds".format(timeout)) self.sync_mempools(timeout=timeout) def test_in_flight_max(self, context): max_getdata_in_flight = context.constants.max_getdata_in_flight max_inbound_delay = context.constants.inbound_peer_delay + \ context.constants.overloaded_peer_delay self.log.info("Test that we don't load peers with more than {} getdata requests immediately".format( max_getdata_in_flight)) invids = [i for i in range(max_getdata_in_flight + 2)] p = self.nodes[0].p2ps[0] with p2p_lock: p.getdata_count = 0 mock_time = int(time.time() + 1) self.nodes[0].setmocktime(mock_time) for i in range(max_getdata_in_flight): p.send_message(msg_inv([CInv(t=context.inv_type, h=invids[i])])) p.sync_with_ping() mock_time += context.constants.inbound_peer_delay self.nodes[0].setmocktime(mock_time) p.wait_until(lambda: p.getdata_count >= max_getdata_in_flight) for i in range(max_getdata_in_flight, len(invids)): p.send_message(msg_inv([CInv(t=context.inv_type, h=invids[i])])) p.sync_with_ping() self.log.info( "No more than {} requests should be seen within {} seconds after announcement".format( max_getdata_in_flight, max_inbound_delay - 1)) self.nodes[0].setmocktime( mock_time + max_inbound_delay - 1) p.sync_with_ping() with p2p_lock: assert_equal(p.getdata_count, max_getdata_in_flight) self.log.info( "If we wait {} seconds after announcement, we should eventually get more requests".format( max_inbound_delay)) self.nodes[0].setmocktime( mock_time + max_inbound_delay) p.wait_until(lambda: p.getdata_count == len(invids)) def test_expiry_fallback(self, context): self.log.info( 'Check that expiry will select another peer for download') peer1 = self.nodes[0].add_p2p_connection(context.p2p_conn()) peer2 = self.nodes[0].add_p2p_connection(context.p2p_conn()) for p in [peer1, peer2]: p.send_message(msg_inv([CInv(t=context.inv_type, h=0xffaa)])) # One of the peers is asked for the data peer2.wait_until( lambda: sum( p.getdata_count for p in [ peer1, peer2]) == 1) with p2p_lock: peer_expiry, peer_fallback = ( peer1, peer2) if peer1.getdata_count == 1 else ( peer2, peer1) assert_equal(peer_fallback.getdata_count, 0) # Wait for request to peer_expiry to expire self.nodes[0].setmocktime( int(time.time()) + context.constants.getdata_interval + 1) peer_fallback.wait_until( lambda: peer_fallback.getdata_count >= 1, timeout=1) with p2p_lock: assert_equal(peer_fallback.getdata_count, 1) # reset mocktime self.restart_node(0) def test_disconnect_fallback(self, context): self.log.info( 'Check that disconnect will select another peer for download') peer1 = self.nodes[0].add_p2p_connection(context.p2p_conn()) peer2 = self.nodes[0].add_p2p_connection(context.p2p_conn()) for p in [peer1, peer2]: p.send_message(msg_inv([CInv(t=context.inv_type, h=0xffbb)])) # One of the peers is asked for the data peer2.wait_until( lambda: sum( p.getdata_count for p in [ peer1, peer2]) == 1) with p2p_lock: peer_disconnect, peer_fallback = ( peer1, peer2) if peer1.getdata_count == 1 else ( peer2, peer1) assert_equal(peer_fallback.getdata_count, 0) peer_disconnect.peer_disconnect() peer_disconnect.wait_for_disconnect() peer_fallback.wait_until( lambda: peer_fallback.getdata_count >= 1, timeout=1) with p2p_lock: assert_equal(peer_fallback.getdata_count, 1) def test_notfound_fallback(self, context): self.log.info( 'Check that notfounds will select another peer for download immediately') peer1 = self.nodes[0].add_p2p_connection(context.p2p_conn()) peer2 = self.nodes[0].add_p2p_connection(context.p2p_conn()) for p in [peer1, peer2]: p.send_message(msg_inv([CInv(t=context.inv_type, h=0xffdd)])) # One of the peers is asked for the data peer2.wait_until( lambda: sum( p.getdata_count for p in [ peer1, peer2]) == 1) with p2p_lock: peer_notfound, peer_fallback = ( peer1, peer2) if peer1.getdata_count == 1 else ( peer2, peer1) assert_equal(peer_fallback.getdata_count, 0) # Send notfound, so that fallback peer is selected peer_notfound.send_and_ping(msg_notfound( vec=[CInv(context.inv_type, 0xffdd)])) peer_fallback.wait_until( lambda: peer_fallback.getdata_count >= 1, timeout=1) with p2p_lock: assert_equal(peer_fallback.getdata_count, 1) def test_preferred_inv(self, context): self.log.info( 'Check that invs from preferred peers are downloaded immediately') self.restart_node( 0, extra_args=self.extra_args[0] + ['-whitelist=noban@127.0.0.1']) peer = self.nodes[0].add_p2p_connection(context.p2p_conn()) peer.send_message(msg_inv([CInv(t=context.inv_type, h=0xff00ff00)])) peer.wait_until(lambda: peer.getdata_count >= 1, timeout=1) with p2p_lock: assert_equal(peer.getdata_count, 1) def test_large_inv_batch(self, context): max_peer_announcements = context.constants.max_peer_announcements net_permissions = context.constants.bypass_request_limits_permission_flags self.log.info( 'Test how large inv batches are handled with {} permission'.format(net_permissions)) self.restart_node( 0, extra_args=self.extra_args[0] + ['-whitelist={}@127.0.0.1'.format(net_permissions)]) peer = self.nodes[0].add_p2p_connection(context.p2p_conn()) peer.send_message(msg_inv([CInv(t=context.inv_type, h=invid) for invid in range(max_peer_announcements + 1)])) peer.wait_until(lambda: peer.getdata_count == max_peer_announcements + 1) self.log.info( 'Test how large inv batches are handled without {} permission'.format(net_permissions)) self.restart_node(0) peer = self.nodes[0].add_p2p_connection(context.p2p_conn()) peer.send_message(msg_inv([CInv(t=context.inv_type, h=invid) for invid in range(max_peer_announcements + 1)])) peer.wait_until(lambda: peer.getdata_count == max_peer_announcements) peer.sync_with_ping() with p2p_lock: assert_equal(peer.getdata_count, max_peer_announcements) def test_spurious_notfound(self, context): self.log.info('Check that spurious notfound is ignored') self.nodes[0].p2ps[0].send_message( msg_notfound(vec=[CInv(context.inv_type, 1)])) @skip(TX_TEST_CONTEXT) def test_orphan_download(self, context): node = self.nodes[0] privkey = ECKey() privkey.generate() privkey_wif = bytes_to_wif(privkey.get_bytes()) # Build a proof with missing utxos so it will be orphaned orphan = node.buildavalancheproof( 42, 2000000000, privkey.get_pubkey().get_bytes().hex(), [{ 'txid': '0' * 64, 'vout': 0, 'amount': 10e6, 'height': 42, 'iscoinbase': False, 'privatekey': privkey_wif, }] ) proofid = FromHex(AvalancheProof(), orphan).proofid proofid_hex = "{:064x}".format(proofid) self.restart_node(0, extra_args=self.extra_args[0] + [ "-avaproof={}".format(orphan), "-avamasterkey={}".format(privkey_wif), ]) - node.generate(2) + node.generate(1) wait_for_proof(node, proofid_hex, expect_orphan=True) peer = node.add_p2p_connection(context.p2p_conn()) peer.send_message(msg_inv([CInv(t=context.inv_type, h=proofid)])) # Give enough time for the node to eventually request the proof. node.setmocktime(int(time.time()) + context.constants.getdata_interval + 1) peer.sync_with_ping() assert_equal(peer.getdata_count, 0) @skip(TX_TEST_CONTEXT) def test_request_invalid_once(self, context): node = self.nodes[0] privkey = ECKey() privkey.generate() # Build an invalid proof (no stake) no_stake_hex = node.buildavalancheproof( 42, 2000000000, privkey.get_pubkey().get_bytes().hex(), [] ) no_stake = FromHex(AvalancheProof(), no_stake_hex) assert_raises_rpc_error(-8, "The proof is invalid: no-stake", node.verifyavalancheproof, no_stake_hex) # Send the proof msg = msg_avaproof() msg.proof = no_stake node.p2ps[0].send_message(msg) # Check we get banned node.p2ps[0].wait_for_disconnect() # Now that the node knows the proof is invalid, it should not be # requested anymore node.p2ps[1].send_message( msg_inv([CInv(t=context.inv_type, h=no_stake.proofid)])) # Give enough time for the node to eventually request the proof node.setmocktime(int(time.time()) + context.constants.getdata_interval + 1) node.p2ps[1].sync_with_ping() assert all(p.getdata_count == 0 for p in node.p2ps[1:]) def run_test(self): for context in [TX_TEST_CONTEXT, PROOF_TEST_CONTEXT]: self.log.info( "Starting tests using " + context.inv_name + " inventory type") # Run tests without mocktime that only need one peer-connection first, # to avoid restarting the nodes self.test_expiry_fallback(context) self.test_disconnect_fallback(context) self.test_notfound_fallback(context) self.test_preferred_inv(context) self.test_large_inv_batch(context) self.test_spurious_notfound(context) # Run each test against new bitcoind instances, as setting mocktimes has long-term effects on when # the next trickle relay event happens. for test in [self.test_in_flight_max, self.test_inv_block, self.test_data_requests, self.test_orphan_download, self.test_request_invalid_once]: self.stop_nodes() self.start_nodes() self.connect_nodes(1, 0) # Setup the p2p connections self.peers = [] for node in self.nodes: for _ in range(NUM_INBOUND): self.peers.append( node.add_p2p_connection( context.p2p_conn())) self.log.info( "Nodes are setup with {} incoming connections each".format(NUM_INBOUND)) test(context) if __name__ == '__main__': InventoryDownloadTest().main()