diff --git a/test/functional/abc_p2p_avalanche.py b/test/functional/abc_p2p_avalanche.py index c30770bd4..13f113e03 100755 --- a/test/functional/abc_p2p_avalanche.py +++ b/test/functional/abc_p2p_avalanche.py @@ -1,473 +1,478 @@ #!/usr/bin/env python3 # Copyright (c) 2020-2021 The Bitcoin developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test the resolution of forks via avalanche.""" import random import struct import time -from test_framework.avatools import create_coinbase_stakes +from test_framework.avatools import create_coinbase_stakes, get_proof_ids from test_framework.key import ( bytes_to_wif, ECKey, ECPubKey, ) from test_framework.p2p import P2PInterface, p2p_lock from test_framework.messages import ( AvalancheDelegation, AvalancheProof, AvalancheResponse, AvalancheVote, CInv, FromHex, hash256, msg_avahello, msg_avapoll, MSG_AVA_PROOF, msg_getdata, msg_tcpavaresponse, NODE_AVALANCHE, NODE_NETWORK, TCPAvalancheResponse, ) from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, wait_until, ) BLOCK_ACCEPTED = 0 BLOCK_INVALID = 1 BLOCK_PARKED = 2 BLOCK_FORK = 3 BLOCK_UNKNOWN = -1 BLOCK_MISSING = -2 BLOCK_PENDING = -3 QUORUM_NODE_COUNT = 16 UNCONDITIONAL_RELAY_DELAY = 2 * 60 class TestNode(P2PInterface): def __init__(self): self.round = 0 self.avahello = None self.avaresponses = [] self.avapolls = [] 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): wait_until( lambda: len(self.avaresponses) > 0, timeout=timeout, lock=p2p_lock) 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): wait_until( lambda: self.avahello is not None, timeout=timeout, lock=p2p_lock) 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(" 2): unexpected-ava-response']): # unknown voting round poll_node.send_avaresponse( round=2**32 - 1, votes=[], privkey=privkey) 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) # 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) self.log.info("Test the avahello signature (node -> P2PInterface)") good_interface = get_node() avahello = good_interface.wait_for_avahello().hello avakey.set(bytes.fromhex(node.getavalanchekey())) assert avakey.verify_schnorr( avahello.sig, avahello.get_sighash(good_interface)) 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_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') 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') - # Connect some blocks to trigger the proof verification - node.generate(2) - node_proofid = FromHex(AvalancheProof(), proof).proofid + + def wait_for_proof_validation(): + # Connect some blocks to trigger the proof verification + node.generate(2) + 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", ]) - node.generate(2) + wait_for_proof_validation() self.log.info( "The proof has not been announced, it cannot be requested") peer = get_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()