diff --git a/test/functional/abc_p2p_avalanche_peer_discovery.py b/test/functional/abc_p2p_avalanche_peer_discovery.py index b511ac827..5d15f78e3 100755 --- a/test/functional/abc_p2p_avalanche_peer_discovery.py +++ b/test/functional/abc_p2p_avalanche_peer_discovery.py @@ -1,259 +1,259 @@ #!/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.address import ADDRESS_ECREG_UNSPENDABLE from test_framework.avatools import ( create_coinbase_stakes, get_ava_p2p_interface, get_proof_ids, ) from test_framework.key import ECKey, ECPubKey from test_framework.messages import ( MSG_AVA_PROOF, NODE_AVALANCHE, NODE_NETWORK, - AvalancheProof, CInv, FromHex, + LegacyAvalancheProof, msg_getdata, ) from test_framework.p2p import p2p_lock from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal from test_framework.wallet_util import bytes_to_wif 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', '-enableavalanchepeerdiscovery=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) wif_privkey = bytes_to_wif(privkey.get_bytes()) 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, wif_privkey, 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 + limited_id = FromHex(LegacyAvalancheProof(), 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, wif_privkey, stakes) limited_id = FromHex( - AvalancheProof(), + LegacyAvalancheProof(), 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 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(peer, proofid): with p2p_lock: return good_interface.last_message.get( "getdata") and good_interface.last_message["getdata"].inv[-1].hash == proofid self.wait_until(lambda: getdata_found(good_interface, proofid)) self.log.info('Check that we can download the proof from our peer') - node_proofid = FromHex(AvalancheProof(), proof).proofid + node_proofid = FromHex(LegacyAvalancheProof(), proof).proofid def wait_for_proof_validation(): # Connect some blocks to trigger the proof verification node.generate(1) self.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 self.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) self.wait_until(lambda: proof_received(peer)) # Restart the node self.restart_node(0, self.extra_args[0] + [ "-avaproof={}".format(proof), "-avamasterkey=cND2ZvtabDbJ1gucx9GWH6XT9kgTAqfb6cotPt5Q5CyxVDhid2EN", ]) wait_for_proof_validation() # The only peer is the node itself assert_equal(len(node.getavalanchepeerinfo()), 1) assert_equal(node.getavalanchepeerinfo()[0]["proof"], proof) peer = get_ava_p2p_interface(node) peer_proofid = peer.send_avahello( interface_delegation_hex, delegated_key) self.wait_until(lambda: getdata_found(peer, peer_proofid)) assert peer_proofid not in get_proof_ids(node) self.log.info( "Check that the peer gets added as an avalanche node as soon as the node knows about the proof") node.sendavalancheproof(interface_proof_hex) def has_node_count(count): peerinfo = node.getavalanchepeerinfo() return (len(peerinfo) == 2 and peerinfo[-1]["proof"] == interface_proof_hex and peerinfo[-1]["nodecount"] == count) self.wait_until(lambda: has_node_count(1)) self.log.info( "Check that the peer gets added immediately if the proof is already known") # Connect another peer using the same proof peer_proof_known = get_ava_p2p_interface(node) peer_proof_known.send_avahello(interface_delegation_hex, delegated_key) self.wait_until(lambda: has_node_count(2)) self.log.info("Invalidate the proof and check the nodes are removed") tip = node.getbestblockhash() # Invalidate the block with the proof utxo node.invalidateblock(blockhashes[1]) # Change the address to make sure we don't generate a block identical # to the one we just invalidated. Can be generate(1) after D9694 or # D9697 is landed. forked_tip = node.generatetoaddress(1, ADDRESS_ECREG_UNSPENDABLE)[0] self.wait_until(lambda: node.getbestblockhash() == forked_tip) self.wait_until(lambda: len(node.getavalanchepeerinfo()) == 1) assert peer_proofid not in get_proof_ids(node) self.log.info("Reorg back and check the nodes are added back") node.invalidateblock(forked_tip) node.reconsiderblock(tip) self.wait_until(lambda: has_node_count(2), timeout=2) if __name__ == '__main__': AvalancheTest().main() diff --git a/test/functional/abc_p2p_proof_inventory.py b/test/functional/abc_p2p_proof_inventory.py index a41446632..e89732d2b 100644 --- a/test/functional/abc_p2p_proof_inventory.py +++ b/test/functional/abc_p2p_proof_inventory.py @@ -1,295 +1,295 @@ #!/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 """ import time from test_framework.address import ADDRESS_ECREG_UNSPENDABLE from test_framework.avatools import gen_proof, get_proof_ids, wait_for_proof from test_framework.key import ECKey from test_framework.messages import ( MSG_AVA_PROOF, MSG_TYPE_MASK, - AvalancheProof, CInv, FromHex, + LegacyAvalancheProof, 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, ) from test_framework.wallet_util import bytes_to_wif # 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 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 = 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 self.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 self.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 = gen_proof(node) peer = node.add_p2p_connection(P2PInterface()) msg = msg_avaproof() msg.proof = proof peer.send_message(msg) self.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, bytes_to_wif(privkey.get_bytes()), [{ 'txid': '0' * 64, 'vout': 0, 'amount': 10e6, 'height': 42, 'iscoinbase': False, 'privatekey': bytes_to_wif(privkey.get_bytes()), }] ) - orphan = FromHex(AvalancheProof(), orphan_hex) + orphan = FromHex(LegacyAvalancheProof(), 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 = 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 = gen_proof(node) proofids.add(proof.proofid) self.restart_node(node.index, self.extra_args[node.index] + [ "-avaproof={}".format(proof.serialize().hex()), "-avamasterkey={}".format(bytes_to_wif(privkey.get_bytes())) ]) # Connect a block to make the proof be added to our pool node.generate(1) self.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_manually_sent_proof(self): node0 = self.nodes[0] _, proof = gen_proof(node0) self.log.info( "Send a proof via RPC and check all the nodes download it") node0.sendavalancheproof(proof.serialize().hex()) self.sync_proofs() def test_unbroadcast(self): self.log.info("Test broadcasting proofs") node = self.nodes[0] # Disconnect the other nodes, or they will request the proof and # invalidate the test [node.stop_node() for node in self.nodes[1:]] 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 = 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) self.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() self.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()) 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) # 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_ECREG_UNSPENDABLE: 25_000_000 - 250.00}, ) 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 self.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_manually_sent_proof() # Run this test last because it needs to disconnect the nodes self.test_unbroadcast() if __name__ == '__main__': ProofInventoryTest().main() diff --git a/test/functional/abc_rpc_addavalanchenode.py b/test/functional/abc_rpc_addavalanchenode.py index abe352dda..26837ca4b 100644 --- a/test/functional/abc_rpc_addavalanchenode.py +++ b/test/functional/abc_rpc_addavalanchenode.py @@ -1,188 +1,188 @@ #!/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 addavalanchenode RPC""" from test_framework.avatools import create_coinbase_stakes from test_framework.key import ECKey from test_framework.messages import ( AvalancheDelegation, AvalancheDelegationLevel, - AvalancheProof, FromHex, + LegacyAvalancheProof, hash256, ser_string, ) from test_framework.p2p import P2PInterface from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_raises_rpc_error from test_framework.wallet_util import bytes_to_wif def add_interface_node(test_node) -> int: """Create a peer, connect it to test_node, return the nodeid of the peer as registered by test_node. """ n = P2PInterface() test_node.add_p2p_connection(n) n.wait_for_verack() return test_node.getpeerinfo()[-1]['id'] class AddAvalancheNodeTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 1 self.extra_args = [['-enableavalanche=1', '-avacooldown=0']] def run_test(self): node = self.nodes[0] addrkey0 = node.get_deterministic_priv_key() blockhashes = node.generatetoaddress(2, addrkey0.address) stakes = create_coinbase_stakes(node, [blockhashes[0]], addrkey0.key) privkey = ECKey() privkey.generate() wif_privkey = bytes_to_wif(privkey.get_bytes()) proof_master = privkey.get_pubkey().get_bytes().hex() proof_sequence = 42 proof_expiration = 2000000000 proof = node.buildavalancheproof( proof_sequence, proof_expiration, wif_privkey, stakes) nodeid = add_interface_node(node) def check_addavalanchenode_error( error_code, error_message, nodeid=nodeid, proof=proof, pubkey=proof_master, delegation=None): assert_raises_rpc_error( error_code, error_message, node.addavalanchenode, nodeid, pubkey, proof, delegation, ) self.log.info("Invalid proof") check_addavalanchenode_error(-22, "Proof must be an hexadecimal string", proof="not a proof") check_addavalanchenode_error(-22, "Proof has invalid format", proof="f000") no_stake = node.buildavalancheproof( proof_sequence, proof_expiration, wif_privkey, []) check_addavalanchenode_error(-8, "The proof is invalid: no-stake", proof=no_stake) self.log.info("Node doesn't exist") check_addavalanchenode_error(-8, f"The node does not exist: {nodeid + 1}", nodeid=nodeid + 1) self.log.info("Invalid delegation") dg_privkey = ECKey() dg_privkey.generate() dg_pubkey = dg_privkey.get_pubkey().get_bytes() check_addavalanchenode_error(-22, "Delegation must be an hexadecimal string", pubkey=dg_pubkey.hex(), delegation="not a delegation") check_addavalanchenode_error(-22, "Delegation has invalid format", pubkey=dg_pubkey.hex(), delegation="f000") self.log.info("Delegation mismatch with the proof") delegation_wrong_proofid = AvalancheDelegation() check_addavalanchenode_error(-8, "The delegation does not match the proof", pubkey=dg_pubkey.hex(), delegation=delegation_wrong_proofid.serialize().hex()) - proofobj = FromHex(AvalancheProof(), proof) + proofobj = FromHex(LegacyAvalancheProof(), proof) delegation = AvalancheDelegation( limited_proofid=proofobj.limited_proofid, proof_master=proofobj.master, ) self.log.info("Delegation with bad signature") bad_level = AvalancheDelegationLevel( pubkey=dg_pubkey, ) delegation.levels.append(bad_level) check_addavalanchenode_error(-8, "The delegation is invalid", pubkey=dg_pubkey.hex(), delegation=delegation.serialize().hex()) delegation.levels = [] level = AvalancheDelegationLevel( pubkey=dg_pubkey, sig=privkey.sign_schnorr( hash256( delegation.getid() + ser_string(dg_pubkey) ) ) ) delegation.levels.append(level) self.log.info("Key mismatch with the proof") check_addavalanchenode_error( -5, "The public key does not match the proof", pubkey=dg_pubkey.hex(), ) self.log.info("Key mismatch with the delegation") random_privkey = ECKey() random_privkey.generate() random_pubkey = random_privkey.get_pubkey() check_addavalanchenode_error( -5, "The public key does not match the delegation", pubkey=random_pubkey.get_bytes().hex(), delegation=delegation.serialize().hex(), ) self.log.info("Happy path") assert node.addavalanchenode(nodeid, proof_master, proof) # Adding several times is OK assert node.addavalanchenode(nodeid, proof_master, proof) # Use an hardcoded proof. This will help detecting proof format changes. # Generated using: # stakes = create_coinbase_stakes(node, [blockhashes[1]], addrkey0.key) # hardcoded_proof = node.buildavalancheproof( # proof_sequence, proof_expiration, random_pubkey, stakes) hardcoded_pubkey = "037d20fcfe118296bb53f0a8f87c864e7b9831c4fcd7c6a0bb9a58e0e0f53d5cbc" hardcoded_proof = ( "2a00000000000000009435770000000021037d20fcfe118296bb53f0a8f87c864e" "7b9831c4fcd7c6a0bb9a58e0e0f53d5cbc01683ef49024cf25bb55775b327f5e68" "c79da3a7824dc03df5623c96f4a60158f90000000000f902950000000095010000" "210227d85ba011276cf25b51df6a188b75e604b38770a462b2d0e9fb2fc839ef5d" "3f612834ef0e2545d6359e9f34967c2bb69cb88fe246fed716d998f3f62eba1ef6" "6a547606a7ac14c1b5697f4acc20853b3f99954f4f7b6e9bf8a085616d3adfc7" ) assert node.addavalanchenode(nodeid, hardcoded_pubkey, hardcoded_proof) self.log.info("Add a node with a valid delegation") assert node.addavalanchenode( nodeid, dg_pubkey.hex(), proof, delegation.serialize().hex(), ) self.log.info("Several nodes can share a proof") nodeid2 = add_interface_node(node) assert node.addavalanchenode(nodeid2, proof_master, proof) if __name__ == '__main__': AddAvalancheNodeTest().main() diff --git a/test/functional/abc_rpc_avalancheproof.py b/test/functional/abc_rpc_avalancheproof.py index e3bcadb67..13f7ecb89 100644 --- a/test/functional/abc_rpc_avalancheproof.py +++ b/test/functional/abc_rpc_avalancheproof.py @@ -1,481 +1,481 @@ #!/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_ECREG_UNSPENDABLE from test_framework.avatools import ( create_coinbase_stakes, create_stakes, get_proof_ids, wait_for_proof, ) from test_framework.key import ECKey from test_framework.messages import ( AvalancheDelegation, AvalancheDelegationLevel, - AvalancheProof, FromHex, + LegacyAvalancheProof, ) 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, assert_raises_rpc_error, connect_nodes, ) from test_framework.wallet_util import bytes_to_wif 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): +class LegacyAvalancheProofTest(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']] self.supports_cli = False self.rpc_timeout = 120 def run_test(self): # Turn off node 1 while node 0 mines blocks to generate stakes, # so that we can later try starting node 1 with an orphan proof. self.stop_node(1) 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) wif_privkey = bytes_to_wif(privkey.get_bytes()) 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, wif_privkey, stakes) self.log.info("Test decodeavalancheproof RPC") - proofobj = FromHex(AvalancheProof(), proof) + proofobj = FromHex(LegacyAvalancheProof(), 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 with this proof self.restart_node(0, self.extra_args[0] + [ "-avaproof={}".format(proof), "-avamasterkey=cND2ZvtabDbJ1gucx9GWH6XT9kgTAqfb6cotPt5Q5CyxVDhid2EN", ]) self.log.info("The proof is registered at first chaintip update") assert_equal(len(node.getavalanchepeerinfo()), 0) node.generate(1) self.wait_until(lambda: len(node.getavalanchepeerinfo()) == 1, timeout=5) # This case will occur for users building proofs with a third party # tool and then starting a new node that is not yet aware of the # transactions used for stakes. self.log.info("Start a node with an orphan proof") self.start_node(1, self.extra_args[0] + [ "-avaproof={}".format(proof), "-avamasterkey=cND2ZvtabDbJ1gucx9GWH6XT9kgTAqfb6cotPt5Q5CyxVDhid2EN", ]) # Mine a block to trigger an attempt at registering the proof self.nodes[1].generate(1) wait_for_proof(self.nodes[1], f"{proofobj.proofid:0{64}x}", expect_orphan=True) self.log.info("Connect to an up-to-date node to unorphan the proof") connect_nodes(self.nodes[1], node) self.sync_all() wait_for_proof(self.nodes[1], f"{proofobj.proofid:0{64}x}", expect_orphan=False) 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, wif_privkey, []) # 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 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 delegation is invalid", 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(-5, "The 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, wif_privkey, 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, wif_privkey, create_coinbase_stakes(node, [blockhashes[0]], addrkey0.key, amount=str(dust_amount))) missing_stake = node.buildavalancheproof( proof_sequence, proof_expiration, wif_privkey, [{ 'txid': '0' * 64, 'vout': 0, 'amount': 10000000, 'height': 42, 'iscoinbase': False, 'privatekey': addrkey0.key, }] ) duplicate_stake = ("0b000000000000000c0000000000000021030b4c866585dd868" "a9d62348a9cd008d6a312937048fff31670e7e920cfc7a74402" "05c5f72f5d6da3085583e75ee79340eb4eff208c89988e7ed0e" "fb30b87298fa30000000000f2052a0100000003000000210227" "d85ba011276cf25b51df6a188b75e604b38770a462b2d0e9fb2" "fc839ef5d3f86076def2e8bc3c40671c1a0eb505da5857a950a" "0cf4625a80018cdd75ac62e61273ff8142f747de67e73f6368c" "8648942b0ef6c065d72a81ad7438a23c11cca05c5f72f5d6da3" "085583e75ee79340eb4eff208c89988e7ed0efb30b87298fa30" "000000000f2052a0100000003000000210227d85ba011276cf2" "5b51df6a188b75e604b38770a462b2d0e9fb2fc839ef5d3f860" "76def2e8bc3c40671c1a0eb505da5857a950a0cf4625a80018c" "dd75ac62e61273ff8142f747de67e73f6368c8648942b0ef6c0" "65d72a81ad7438a23c11cca") bad_sig = ("0b000000000000000c0000000000000021030b4c866585dd868a9d62348" "a9cd008d6a312937048fff31670e7e920cfc7a7440105c5f72f5d6da3085" "583e75ee79340eb4eff208c89988e7ed0efb30b87298fa30000000000f20" "52a0100000003000000210227d85ba011276cf25b51df6a188b75e604b3" "8770a462b2d0e9fb2fc839ef5d3faf07f001dd38e9b4a43d07d5d449cc0" "f7d2888d96b82962b3ce516d1083c0e031773487fc3c4f2e38acd1db974" "1321b91a79b82d1c2cfd47793261e4ba003cf5") wrong_order = ("c964aa6fde575e4ce8404581c7be874e21023beefdde700a6bc0203" "6335b4df141c8bc67bb05a971f5ac2745fd683797dde30305d427b7" "06705a5d4b6a368a231d6db62abacf8c29bc32b61e7f65a0a6976aa" "8b86b687bc0260e821e4f0200b9d3bf6d2102449fb5237efe8f647d" "32e8b64f06c22d1d40368eaca2a71ffc6a13ecc8bce68052365271b" "6c71189f5cd7e3b694b77b579080f0b35bae567b96590ab6aa3019b" "018ff9f061f52f1426bdb195d4b6d4dff5114cee90e33dabf0c588e" "badf7774418f54247f6390791706af36fac782302479898b5273f9e" "51a92cb1fb5af43deeb6c8c269403d30ffcb380300134398c42103e" "49f9df52de2dea81cf7838b82521b69f2ea360f1c4eed9e6c89b7d0" "f9e645efa08e97ea0c60e1f0a064fbf08989c084707082727e85dcb" "9f79bb503f76ee6c8dad42a07ef15c89b3750a5631d604b21fafff0" "f4de354ade95c2f28160ae549af0d4ce48c4ca9d0714b1fa5192027" "0f8575e0af610f07b4e602a018ecdbb649b64fff614c0026e9fc8e0" "030092533d422103aac52f4cfca700e7e9824298e0184755112e32f" "359c832f5f6ad2ef62a2c024af812d6d7f2ecc6223a774e19bce1fb" "20d94d6b01ea693638f55c74fdaa5358fa9239d03e4caf3d817e8f7" "48ccad55a27b9d365db06ad5a0b779ac385f3dc8710") self.log.info( "Check the verifyavalancheproof and sendavalancheproof RPCs") if self.is_wallet_compiled(): self.log.info( "Check a proof with the maximum number of UTXO is valid") 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, wif_privkey, maximum_stakes) too_many_utxos = node.buildavalancheproof( proof_sequence, proof_expiration, wif_privkey, too_many_stakes) assert node.verifyavalancheproof(good_proof) 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-stake-signature") check_rpc_failure(wrong_order, "wrong-stake-ordering") if self.is_wallet_compiled(): check_rpc_failure(too_many_utxos, "too-many-utxos") conflicting_utxo = node.buildavalancheproof( proof_sequence + 1, proof_expiration, wif_privkey, 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 + proofid = FromHex(LegacyAvalancheProof(), 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 self.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_ECREG_UNSPENDABLE: stakes[-1] ["amount"] - Decimal('10000')} ) signed_tx = node.signrawtransactionwithkey(raw_tx, [addrkey0.key]) node.sendrawtransaction(signed_tx["hex"]) node.generate(1) self.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() + LegacyAvalancheProofTest().main() diff --git a/test/functional/p2p_eviction.py b/test/functional/p2p_eviction.py index bb377fc07..c7fb4369c 100755 --- a/test/functional/p2p_eviction.py +++ b/test/functional/p2p_eviction.py @@ -1,212 +1,212 @@ #!/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 node eviction logic When the number of peers has reached the limit of maximum connections, the next connecting inbound peer will trigger the eviction mechanism. We cannot currently test the parts of the eviction logic that are based on address/netgroup since in the current framework, all peers are connecting from the same local address. See Issue #14210 for more info. Therefore, this test is limited to the remaining protection criteria. """ import time from test_framework.avatools import AvaP2PInterface, create_coinbase_stakes from test_framework.blocktools import create_block, create_coinbase from test_framework.key import ECKey from test_framework.messages import ( - AvalancheProof, CTransaction, FromHex, + LegacyAvalancheProof, msg_avaproof, msg_pong, msg_tx, ) from test_framework.p2p import P2PDataStore, P2PInterface from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal from test_framework.wallet_util import bytes_to_wif class SlowP2PDataStore(P2PDataStore): def on_ping(self, message): time.sleep(0.1) self.send_message(msg_pong(message.nonce)) class SlowP2PInterface(P2PInterface): def on_ping(self, message): time.sleep(0.1) self.send_message(msg_pong(message.nonce)) class SlowAvaP2PInterface(AvaP2PInterface): def on_ping(self, message): time.sleep(0.1) self.send_message(msg_pong(message.nonce)) class P2PEvict(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 1 # The choice of maxconnections=164 results in a maximum of 153 inbound # connections (164 - 10 outbound - 1 feeler). 152 inbound peers are # protected from eviction: # 4 by netgroup, 4 that sent us blocks, 4 that sent us proofs, 4 that # sent us transactions, 8 via lowest ping time, 128 with the best # avalanche availability score self.extra_args = [['-maxconnections=164', "-enableavalanche=1"]] def run_test(self): # peers that we expect to be protected from eviction protected_peers = set() current_peer = -1 node = self.nodes[0] blocks = node.generatetoaddress( 101, node.get_deterministic_priv_key().address) self.log.info( "Create 4 peers and protect them from eviction by sending us a block") for _ in range(4): block_peer = node.add_p2p_connection(SlowP2PDataStore()) current_peer += 1 block_peer.sync_with_ping() best_block = node.getbestblockhash() tip = int(best_block, 16) best_block_time = node.getblock(best_block)['time'] block = create_block( tip, create_coinbase( node.getblockcount() + 1), best_block_time + 1) block.solve() block_peer.send_blocks_and_test([block], node, success=True) protected_peers.add(current_peer) self.log.info( "Create 4 peers and protect them from eviction by sending us a proof") privkey = ECKey() privkey.generate() wif_privkey = bytes_to_wif(privkey.get_bytes()) pubkey = privkey.get_pubkey() stakes = create_coinbase_stakes( node, blocks, node.get_deterministic_priv_key().key) for i in range(4): proof_peer = node.add_p2p_connection(SlowP2PDataStore()) current_peer += 1 proof_peer.sync_with_ping() proof = node.buildavalancheproof( 42, 2000000000, wif_privkey, [stakes[i]]) avaproof_msg = msg_avaproof() - avaproof_msg.proof = FromHex(AvalancheProof(), proof) + avaproof_msg.proof = FromHex(LegacyAvalancheProof(), proof) proof_peer.send_message(avaproof_msg) protected_peers.add(current_peer) self.log.info( "Create 5 slow-pinging peers, making them eviction candidates") for _ in range(5): node.add_p2p_connection(SlowP2PInterface()) current_peer += 1 self.log.info( "Create 4 peers and protect them from eviction by sending us a tx") for i in range(4): txpeer = node.add_p2p_connection(SlowP2PInterface()) current_peer += 1 txpeer.sync_with_ping() prevtx = node.getblock(node.getblockhash(i + 1), 2)['tx'][0] rawtx = node.createrawtransaction( inputs=[{'txid': prevtx['txid'], 'vout': 0}], outputs=[ {node.get_deterministic_priv_key().address: 50000000 - 1250.00}], ) sigtx = node.signrawtransactionwithkey( hexstring=rawtx, privkeys=[node.get_deterministic_priv_key().key], prevtxs=[{ 'txid': prevtx['txid'], 'vout': 0, 'amount': prevtx['vout'][0]['value'], 'scriptPubKey': prevtx['vout'][0]['scriptPubKey']['hex'], }], )['hex'] txpeer.send_message(msg_tx(FromHex(CTransaction(), sigtx))) protected_peers.add(current_peer) self.log.info( "Create 8 peers and protect them from eviction by having faster pings") for _ in range(8): fastpeer = node.add_p2p_connection(P2PInterface()) current_peer += 1 self.wait_until(lambda: "ping" in fastpeer.last_message, timeout=10) self.log.info( "Create 128 peers and protect them from eviction by sending an avahello message") proof = node.buildavalancheproof( 42, 2000000000, wif_privkey, [stakes[0]]) - proof_obj = FromHex(AvalancheProof(), proof) + proof_obj = FromHex(LegacyAvalancheProof(), proof) delegation = node.delegateavalancheproof( f"{proof_obj.limited_proofid:064x}", bytes_to_wif(privkey.get_bytes()), pubkey.get_bytes().hex(), ) for _ in range(128): avapeer = node.add_p2p_connection(SlowAvaP2PInterface()) current_peer += 1 avapeer.sync_with_ping() avapeer.send_avahello(delegation, privkey) # Make sure by asking the node what the actual min pings are peerinfo = node.getpeerinfo() pings = {} for i in range(len(peerinfo)): pings[i] = peerinfo[i]['minping'] if 'minping' in peerinfo[i] else 1000000 sorted_pings = sorted(pings.items(), key=lambda x: x[1]) # Usually the 8 fast peers are protected. In rare case of unreliable pings, # one of the slower peers might have a faster min ping though. for i in range(8): protected_peers.add(sorted_pings[i][0]) self.log.info("Create peer that triggers the eviction mechanism") node.add_p2p_connection(SlowP2PInterface()) # One of the non-protected peers must be evicted. We can't be sure which one because # 4 peers are protected via netgroup, which is identical for all peers, # and the eviction mechanism doesn't preserve the order of identical # elements. evicted_peers = [] for i in range(len(node.p2ps)): if not node.p2ps[i].is_connected: evicted_peers.append(i) self.log.info("Test that one peer was evicted") self.log.debug( "{} evicted peer: {}".format( len(evicted_peers), set(evicted_peers))) assert_equal(len(evicted_peers), 1) self.log.info("Test that no peer expected to be protected was evicted") self.log.debug( "{} protected peers: {}".format( len(protected_peers), protected_peers)) assert evicted_peers[0] not in protected_peers if __name__ == '__main__': P2PEvict().main() diff --git a/test/functional/p2p_inv_download.py b/test/functional/p2p_inv_download.py index f2701a3ce..7b8b462f7 100755 --- a/test/functional/p2p_inv_download.py +++ b/test/functional/p2p_inv_download.py @@ -1,487 +1,487 @@ #!/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 """ import functools import time from test_framework.address import ADDRESS_ECREG_UNSPENDABLE from test_framework.avatools import wait_for_proof from test_framework.key import ECKey from test_framework.messages import ( MSG_AVA_PROOF, MSG_TX, MSG_TYPE_MASK, - AvalancheProof, CInv, CTransaction, FromHex, + LegacyAvalancheProof, 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 from test_framework.wallet_util import bytes_to_wif 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 # Common network parameters UNCONDITIONAL_RELAY_DELAY = 2 * 60 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) self.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_tx(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_ECREG_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) # node1 is an inbound peer for node0, so the tx relay is delayed by a # duration calculated using a poisson's law with a 5s average time. # In order to make sure the inv is sent we move the time 2 minutes # forward, which has the added side effect that the tx can be # unconditionally requested. with self.nodes[1].assert_debug_log([f"got inv: tx {txid:064x} new peer=0"]): self.nodes[0].setmocktime( int(time.time()) + UNCONDITIONAL_RELAY_DELAY) # Since node 1 is connected outbound to an honest peer (node 0), it # should get the tx within a 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 max_delay = context.constants.inbound_peer_delay + \ context.constants.getdata_interval margin = 2 self.log.info( "Tx should be received at node 1 after {} seconds".format(max_delay + margin)) self.nodes[1].setmocktime(int(time.time()) + max_delay) self.sync_mempools(timeout=margin) 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) 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) 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) 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) 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, bytes_to_wif(privkey.get_bytes()), [{ 'txid': '0' * 64, 'vout': 0, 'amount': 10e6, 'height': 42, 'iscoinbase': False, 'privatekey': privkey_wif, }] ) - proofid = FromHex(AvalancheProof(), orphan).proofid + proofid = FromHex(LegacyAvalancheProof(), 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(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, bytes_to_wif(privkey.get_bytes()), [] ) - no_stake = FromHex(AvalancheProof(), no_stake_hex) + no_stake = FromHex(LegacyAvalancheProof(), 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_tx, 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() diff --git a/test/functional/test_framework/avatools.py b/test/functional/test_framework/avatools.py index c1bf00049..9ca539dd4 100644 --- a/test/functional/test_framework/avatools.py +++ b/test/functional/test_framework/avatools.py @@ -1,271 +1,271 @@ #!/usr/bin/env python3 # Copyright (c) 2021 The Bitcoin ABC developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Utilities for avalanche tests.""" import struct from typing import Any, Dict, List, Optional from .authproxy import JSONRPCException from .key import ECKey from .messages import ( NODE_AVALANCHE, NODE_NETWORK, AvalancheDelegation, - AvalancheProof, AvalancheResponse, CInv, CTransaction, FromHex, + LegacyAvalancheProof, TCPAvalancheResponse, ToHex, hash256, msg_avahello, msg_avapoll, msg_tcpavaresponse, ) from .p2p import P2PInterface, p2p_lock from .test_node import TestNode from .util import assert_equal, satoshi_round, wait_until_helper from .wallet_util import bytes_to_wif def create_coinbase_stakes( node: TestNode, blockhashes: List[str], priv_key: str, amount: Optional[str] = None) -> List[Dict[str, Any]]: """Returns a list of dictionaries representing stakes, in a format compatible with the buildavalancheproof RPC, using only coinbase transactions. :param node: Test node used to get the block and coinbase data. :param blockhashes: List of block hashes, whose coinbase tx will be used as a stake. :param priv_key: Private key controlling the coinbase UTXO :param amount: If specified, this overwrites the amount information in the coinbase dicts. """ blocks = [node.getblock(h, 2) for h in blockhashes] coinbases = [ { 'height': b['height'], 'txid': b['tx'][0]['txid'], 'n': 0, 'value': b['tx'][0]['vout'][0]['value'], } for b in blocks ] return [{ 'txid': coinbase['txid'], 'vout': coinbase['n'], 'amount': amount or coinbase['value'], 'height': coinbase['height'], 'iscoinbase': True, 'privatekey': priv_key, } for coinbase in coinbases] def get_utxos_in_blocks(node: TestNode, blockhashes: List[str]) -> List[Dict]: """Return all UTXOs in the specified list of blocks. """ utxos = filter( lambda u: node.gettransaction(u["txid"])["blockhash"] in blockhashes, node.listunspent()) return list(utxos) def create_stakes( node: TestNode, blockhashes: List[str], count: int ) -> List[Dict[str, Any]]: """ Create a list of stakes by splitting existing UTXOs from a specified list of blocks into 10 new coins. This function can generate more valid stakes than `get_coinbase_stakes` does, because on the regtest chain halving happens every 150 blocks so the coinbase amount is below the dust threshold after only 900 blocks. :param node: Test node used to generate blocks and send transactions :param blockhashes: List of block hashes whose UTXOs will be split. :param count: Number of stakes to return. """ assert 10 * len(blockhashes) >= count utxos = get_utxos_in_blocks(node, blockhashes) addresses = [node.getnewaddress() for _ in range(10)] private_keys = {addr: node.dumpprivkey(addr) for addr in addresses} for u in utxos: inputs = [{"txid": u["txid"], "vout": u["vout"]}] outputs = { addr: satoshi_round(u['amount'] / 10) for addr in addresses} raw_tx = node.createrawtransaction(inputs, outputs) ctx = FromHex(CTransaction(), raw_tx) ctx.vout[0].nValue -= node.calculate_fee(ctx) signed_tx = node.signrawtransactionwithwallet(ToHex(ctx))["hex"] node.sendrawtransaction(signed_tx) # confirm the transactions new_blocks = [] while node.getmempoolinfo()['size'] > 0: new_blocks += node.generate(1) utxos = get_utxos_in_blocks(node, new_blocks) stakes = [] # cache block heights heights = {} for utxo in utxos[:count]: blockhash = node.gettransaction(utxo["txid"])["blockhash"] if blockhash not in heights: heights[blockhash] = node.getblock(blockhash, 1)["height"] stakes.append({ 'txid': utxo['txid'], 'vout': utxo['vout'], 'amount': utxo['amount'], 'iscoinbase': utxo['label'] == "coinbase", 'height': heights[blockhash], 'privatekey': private_keys[utxo["address"]], }) return stakes def get_proof_ids(node): - return [FromHex(AvalancheProof(), peer['proof'] + return [FromHex(LegacyAvalancheProof(), peer['proof'] ).proofid for peer in node.getavalanchepeerinfo()] def wait_for_proof(node, proofid_hex, timeout=60, expect_orphan=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. """ def proof_found(): try: wait_for_proof.is_orphan = node.getrawavalancheproof(proofid_hex)[ "orphan"] 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) class AvaP2PInterface(P2PInterface): """P2PInterface with avalanche capabilities""" def __init__(self): self.round = 0 self.avahello = None self.avaresponses = [] self.avapolls = [] self.nodeid: Optional[int] = None super().__init__() def peer_connect(self, *args, **kwargs): create_conn = super().peer_connect(*args, **kwargs) # Save the nonce and extra entropy so they can be reused later. self.local_nonce = self.on_connection_send_msg.nNonce self.local_extra_entropy = self.on_connection_send_msg.nExtraEntropy return create_conn def on_version(self, message): super().on_version(message) # Save the nonce and extra entropy so they can be reused later. self.remote_nonce = message.nNonce self.remote_extra_entropy = message.nExtraEntropy def on_avaresponse(self, message): self.avaresponses.append(message.response) def on_avapoll(self, message): self.avapolls.append(message.poll) def on_avahello(self, message): assert(self.avahello is None) self.avahello = message def send_avaresponse(self, round, votes, privkey): response = AvalancheResponse(round, 0, votes) sig = privkey.sign_schnorr(response.get_hash()) msg = msg_tcpavaresponse() msg.response = TCPAvalancheResponse(response, sig) self.send_message(msg) def wait_for_avaresponse(self, timeout=5): self.wait_until( lambda: len(self.avaresponses) > 0, timeout=timeout) with p2p_lock: return self.avaresponses.pop(0) def send_poll(self, hashes): msg = msg_avapoll() msg.poll.round = self.round self.round += 1 for h in hashes: msg.poll.invs.append(CInv(2, h)) self.send_message(msg) def get_avapoll_if_available(self): with p2p_lock: return self.avapolls.pop(0) if len(self.avapolls) > 0 else None def wait_for_avahello(self, timeout=5): self.wait_until( lambda: self.avahello is not None, timeout=timeout) with p2p_lock: return self.avahello def send_avahello(self, delegation_hex: str, delegated_privkey: ECKey): delegation = FromHex(AvalancheDelegation(), delegation_hex) local_sighash = hash256( delegation.getid() + struct.pack(" AvaP2PInterface: """Build and return an AvaP2PInterface connected to the specified TestNode. """ n = AvaP2PInterface() node.add_p2p_connection( n, services=services) n.wait_for_verack() n.nodeid = node.getpeerinfo()[-1]['id'] return n def gen_proof(node, coinbase_utxos=1): blockhashes = node.generate(coinbase_utxos) privkey = ECKey() privkey.generate() stakes = create_coinbase_stakes( node, blockhashes, node.get_deterministic_priv_key().key) proof_hex = node.buildavalancheproof( 42, 2000000000, bytes_to_wif(privkey.get_bytes()), stakes) - return privkey, FromHex(AvalancheProof(), proof_hex) + return privkey, FromHex(LegacyAvalancheProof(), proof_hex) diff --git a/test/functional/test_framework/messages.py b/test/functional/test_framework/messages.py index f4e85c5cc..322b66661 100755 --- a/test/functional/test_framework/messages.py +++ b/test/functional/test_framework/messages.py @@ -1,2098 +1,2206 @@ #!/usr/bin/env python3 # Copyright (c) 2010 ArtForz -- public domain half-a-node # Copyright (c) 2012 Jeff Garzik # Copyright (c) 2010-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. """Bitcoin test framework primitive and message structures CBlock, CTransaction, CBlockHeader, CTxIn, CTxOut, etc....: data structures that should map to corresponding structures in bitcoin/primitives msg_block, msg_tx, msg_headers, etc.: data structures that represent network messages ser_*, deser_*: functions that handle serialization/deserialization. Classes use __slots__ to ensure extraneous attributes aren't accidentally added by tests, compromising their intended effect. """ import copy import hashlib import random import socket import struct import time import unittest +from base64 import b64decode, b64encode from codecs import encode from enum import IntEnum from io import BytesIO from typing import List from test_framework.siphash import siphash256 from test_framework.util import assert_equal, hex_str_to_bytes MIN_VERSION_SUPPORTED = 60001 # past bip-31 for ping/pong MY_VERSION = 70014 MY_SUBVERSION = b"/python-p2p-tester:0.0.3/" # from version 70001 onwards, fRelay should be appended to version # messages (BIP37) MY_RELAY = 1 MAX_LOCATOR_SZ = 101 MAX_BLOCK_BASE_SIZE = 1000000 MAX_BLOOM_FILTER_SIZE = 36000 MAX_BLOOM_HASH_FUNCS = 50 # 1,000,000 XEC in satoshis (legacy BCHA) COIN = 100000000 # 1 XEC in satoshis XEC = 100 MAX_MONEY = 21000000 * COIN # Maximum length of incoming protocol messages MAX_PROTOCOL_MESSAGE_LENGTH = 2 * 1024 * 1024 MAX_HEADERS_RESULTS = 2000 # Number of headers sent in one getheaders result MAX_INV_SIZE = 50000 # Maximum number of entries in an 'inv' protocol message NODE_NETWORK = (1 << 0) NODE_GETUTXO = (1 << 1) NODE_BLOOM = (1 << 2) # NODE_WITNESS = (1 << 3) # NODE_XTHIN = (1 << 4) # removed in v0.22.12 NODE_COMPACT_FILTERS = (1 << 6) NODE_NETWORK_LIMITED = (1 << 10) NODE_AVALANCHE = (1 << 24) MSG_TX = 1 MSG_BLOCK = 2 MSG_FILTERED_BLOCK = 3 MSG_CMPCT_BLOCK = 4 MSG_AVA_PROOF = 0x1f000001 MSG_TYPE_MASK = 0xffffffff >> 2 FILTER_TYPE_BASIC = 0 # Serialization/deserialization tools def sha256(s): return hashlib.new('sha256', s).digest() def hash256(s): return sha256(sha256(s)) def ser_compact_size(size): r = b"" if size < 253: r = struct.pack("B", size) elif size < 0x10000: r = struct.pack(">= 32 return rs def uint256_from_str(s): r = 0 t = struct.unpack("> 24) & 0xFF v = (c & 0xFFFFFF) << (8 * (nbytes - 3)) return v # deser_function_name: Allow for an alternate deserialization function on the # entries in the vector. def deser_vector(f, c, deser_function_name=None): nit = deser_compact_size(f) r = [] for _ in range(nit): t = c() if deser_function_name: getattr(t, deser_function_name)(f) else: t.deserialize(f) r.append(t) return r # ser_function_name: Allow for an alternate serialization function on the # entries in the vector. def ser_vector(v, ser_function_name=None): r = ser_compact_size(len(v)) for i in v: if ser_function_name: r += getattr(i, ser_function_name)() else: r += i.serialize() return r def deser_uint256_vector(f): nit = deser_compact_size(f) r = [] for _ in range(nit): t = deser_uint256(f) r.append(t) return r def ser_uint256_vector(v): r = ser_compact_size(len(v)) for i in v: r += ser_uint256(i) return r def deser_string_vector(f): nit = deser_compact_size(f) r = [] for _ in range(nit): t = deser_string(f) r.append(t) return r def ser_string_vector(v): r = ser_compact_size(len(v)) for sv in v: r += ser_string(sv) return r def FromHex(obj, hex_string): """Deserialize from a hex string representation (eg from RPC)""" obj.deserialize(BytesIO(hex_str_to_bytes(hex_string))) return obj def ToHex(obj): """Convert a binary-serializable object to hex (eg for submission via RPC)""" return obj.serialize().hex() # Objects that map to bitcoind objects, which can be serialized/deserialized class CAddress: __slots__ = ("net", "ip", "nServices", "port", "time") # see https://github.com/bitcoin/bips/blob/master/bip-0155.mediawiki NET_IPV4 = 1 ADDRV2_NET_NAME = { NET_IPV4: "IPv4" } ADDRV2_ADDRESS_LENGTH = { NET_IPV4: 4 } def __init__(self): self.time = 0 self.nServices = 1 self.net = self.NET_IPV4 self.ip = "0.0.0.0" self.port = 0 def deserialize(self, f, *, with_time=True): """Deserialize from addrv1 format (pre-BIP155)""" if with_time: # VERSION messages serialize CAddress objects without time self.time = struct.unpack("H", f.read(2))[0] def serialize(self, *, with_time=True): """Serialize in addrv1 format (pre-BIP155)""" assert self.net == self.NET_IPV4 r = b"" if with_time: # VERSION messages serialize CAddress objects without time r += struct.pack("H", self.port) return r def deserialize_v2(self, f): """Deserialize from addrv2 format (BIP155)""" self.time = struct.unpack("H", f.read(2))[0] def serialize_v2(self): """Serialize in addrv2 format (BIP155)""" assert self.net == self.NET_IPV4 r = b"" r += struct.pack("H", self.port) return r def __repr__(self): return ("CAddress(nServices=%i net=%s addr=%s port=%i)" % (self.nServices, self.ADDRV2_NET_NAME[self.net], self.ip, self.port)) class CInv: __slots__ = ("hash", "type") typemap = { 0: "Error", MSG_TX: "TX", MSG_BLOCK: "Block", MSG_FILTERED_BLOCK: "filtered Block", MSG_CMPCT_BLOCK: "CompactBlock", MSG_AVA_PROOF: "avalanche proof", } def __init__(self, t=0, h=0): self.type = t self.hash = h def deserialize(self, f): self.type = struct.unpack(" MAX_MONEY: return False return True def __repr__(self): return "CTransaction(nVersion={} vin={} vout={} nLockTime={})".format( self.nVersion, repr(self.vin), repr(self.vout), self.nLockTime) class CBlockHeader: __slots__ = ("hash", "hashMerkleRoot", "hashPrevBlock", "nBits", "nNonce", "nTime", "nVersion", "sha256") def __init__(self, header=None): if header is None: self.set_null() else: self.nVersion = header.nVersion self.hashPrevBlock = header.hashPrevBlock self.hashMerkleRoot = header.hashMerkleRoot self.nTime = header.nTime self.nBits = header.nBits self.nNonce = header.nNonce self.sha256 = header.sha256 self.hash = header.hash self.calc_sha256() def set_null(self): self.nVersion = 1 self.hashPrevBlock = 0 self.hashMerkleRoot = 0 self.nTime = 0 self.nBits = 0 self.nNonce = 0 self.sha256 = None self.hash = None def deserialize(self, f): self.nVersion = struct.unpack(" 1: newhashes = [] for i in range(0, len(hashes), 2): i2 = min(i + 1, len(hashes) - 1) newhashes.append(hash256(hashes[i] + hashes[i2])) hashes = newhashes return uint256_from_str(hashes[0]) def calc_merkle_root(self): hashes = [] for tx in self.vtx: tx.calc_sha256() hashes.append(ser_uint256(tx.sha256)) return self.get_merkle_root(hashes) def is_valid(self): self.calc_sha256() target = uint256_from_compact(self.nBits) if self.sha256 > target: return False for tx in self.vtx: if not tx.is_valid(): return False if self.calc_merkle_root() != self.hashMerkleRoot: return False return True def solve(self): self.rehash() target = uint256_from_compact(self.nBits) while self.sha256 > target: self.nNonce += 1 self.rehash() def __repr__(self): return "CBlock(nVersion={} hashPrevBlock={:064x} hashMerkleRoot={:064x} nTime={} nBits={:08x} nNonce={:08x} vtx={})".format( self.nVersion, self.hashPrevBlock, self.hashMerkleRoot, self.nTime, self.nBits, self.nNonce, repr(self.vtx)) class PrefilledTransaction: __slots__ = ("index", "tx") def __init__(self, index=0, tx=None): self.index = index self.tx = tx def deserialize(self, f): self.index = deser_compact_size(f) self.tx = CTransaction() self.tx.deserialize(f) def serialize(self): r = b"" r += ser_compact_size(self.index) r += self.tx.serialize() return r def __repr__(self): return "PrefilledTransaction(index={}, tx={})".format( self.index, repr(self.tx)) # This is what we send on the wire, in a cmpctblock message. class P2PHeaderAndShortIDs: __slots__ = ("header", "nonce", "prefilled_txn", "prefilled_txn_length", "shortids", "shortids_length") def __init__(self): self.header = CBlockHeader() self.nonce = 0 self.shortids_length = 0 self.shortids = [] self.prefilled_txn_length = 0 self.prefilled_txn = [] def deserialize(self, f): self.header.deserialize(f) self.nonce = struct.unpack("> 1 self.pubkey = deser_string(f) def serialize(self) -> bytes: r = self.utxo.serialize() height_ser = self.height << 1 | int(self.is_coinbase) r += struct.pack(' bytes: - """Return the bitcoin hash of the concatenation of proofid - and the serialized stake.""" - return hash256(proofid + self.serialize()) - def __repr__(self): return f"AvalancheStake(utxo={self.utxo}, amount={self.amount}," \ f" height={self.height}, " \ f"pubkey={self.pubkey.hex()})" class AvalancheSignedStake: def __init__(self, stake=None, sig=b""): self.stake: AvalancheStake = stake or AvalancheStake() self.sig: bytes = sig """Signature for this stake, bytes of length 64""" def deserialize(self, f): self.stake = AvalancheStake() self.stake.deserialize(f) self.sig = f.read(64) def serialize(self) -> bytes: return self.stake.serialize() + self.sig class AvalancheProof: __slots__ = ( "sequence", "expiration", "master", "stakes", + "payout_script", + "signature", "limited_proofid", "proofid") def __init__(self, sequence=0, expiration=0, - master=b"", signed_stakes=None): + master=b"", signed_stakes=None, payout_script=b"", signature=b""): self.sequence: int = sequence self.expiration: int = expiration self.master: bytes = master self.stakes: List[AvalancheSignedStake] = signed_stakes or [ AvalancheSignedStake()] + self.payout_script = payout_script + self.signature = signature + self.limited_proofid: int = None self.proofid: int = None self.compute_proof_id() def compute_proof_id(self): """Compute Bitcoin's 256-bit hash (double SHA-256) of the serialized proof data. """ ss = struct.pack(" int: return uint256_from_str(hash256( ser_uint256(self.limited_proofid) + ser_string(self.proof_master))) def deserialize(self, f): self.limited_proofid = deser_uint256(f) self.proof_master = deser_string(f) self.levels = deser_vector(f, AvalancheDelegationLevel) self.proofid = self.compute_proofid() def serialize(self): r = b"" r += ser_uint256(self.limited_proofid) r += ser_string(self.proof_master) r += ser_vector(self.levels) return r def __repr__(self): return f"AvalancheDelegation(limitedProofId={self.limited_proofid:064x}, " \ f"proofMaster={self.proof_master.hex()}, proofid={self.proofid:064x}, " \ f"levels={self.levels})" def getid(self): h = ser_uint256(self.proofid) for level in self.levels: h = hash256(h + ser_string(level.pubkey)) return h class AvalancheHello(): __slots__ = ("delegation", "sig") def __init__(self, delegation=AvalancheDelegation(), sig=b"\0" * 64): self.delegation = delegation self.sig = sig def deserialize(self, f): self.delegation.deserialize(f) self.sig = f.read(64) def serialize(self): r = b"" r += self.delegation.serialize() r += self.sig return r def __repr__(self): return "AvalancheHello(delegation={}, sig={})".format( repr(self.delegation), self.sig) def get_sighash(self, node): b = self.delegation.getid() b += struct.pack(" class msg_headers: __slots__ = ("headers",) msgtype = b"headers" def __init__(self, headers=None): self.headers = headers if headers is not None else [] def deserialize(self, f): # comment in bitcoind indicates these should be deserialized as blocks blocks = deser_vector(f, CBlock) for x in blocks: self.headers.append(CBlockHeader(x)) def serialize(self): blocks = [CBlock(x) for x in self.headers] return ser_vector(blocks) def __repr__(self): return "msg_headers(headers={})".format(repr(self.headers)) class msg_merkleblock: __slots__ = ("merkleblock",) msgtype = b"merkleblock" def __init__(self, merkleblock=None): if merkleblock is None: self.merkleblock = CMerkleBlock() else: self.merkleblock = merkleblock def deserialize(self, f): self.merkleblock.deserialize(f) def serialize(self): return self.merkleblock.serialize() def __repr__(self): return "msg_merkleblock(merkleblock={})".format(repr(self.merkleblock)) class msg_filterload: __slots__ = ("data", "nHashFuncs", "nTweak", "nFlags") msgtype = b"filterload" def __init__(self, data=b'00', nHashFuncs=0, nTweak=0, nFlags=0): self.data = data self.nHashFuncs = nHashFuncs self.nTweak = nTweak self.nFlags = nFlags def deserialize(self, f): self.data = deser_string(f) self.nHashFuncs = struct.unpack("