diff --git a/test/functional/abc_p2p_proof_inventory.py b/test/functional/abc_p2p_proof_inventory.py index 05d409dd0..8aed96638 100644 --- a/test/functional/abc_p2p_proof_inventory.py +++ b/test/functional/abc_p2p_proof_inventory.py @@ -1,303 +1,303 @@ #!/usr/bin/env python3 # Copyright (c) 2021 The Bitcoin developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """ Test proof inventory relaying """ from test_framework.avatools import ( gen_proof, get_proof_ids, wait_for_proof, ) from test_framework.address import ADDRESS_ECREG_UNSPENDABLE from test_framework.key import ECKey from test_framework.messages import ( AvalancheProof, CInv, FromHex, MSG_AVA_PROOF, MSG_TYPE_MASK, msg_avaproof, msg_getdata, ) from test_framework.p2p import ( P2PInterface, p2p_lock, ) from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, assert_greater_than, connect_nodes, wait_until, ) from test_framework.wallet_util import bytes_to_wif import time # Broadcast reattempt occurs every 10 to 15 minutes MAX_INITIAL_BROADCAST_DELAY = 15 * 60 # Delay to allow the node to respond to getdata requests UNCONDITIONAL_RELAY_DELAY = 2 * 60 class ProofInvStoreP2PInterface(P2PInterface): def __init__(self): super().__init__() self.proof_invs_counter = 0 def on_inv(self, message): for i in message.inv: if i.type & MSG_TYPE_MASK == MSG_AVA_PROOF: self.proof_invs_counter += 1 class ProofInventoryTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 5 self.extra_args = [['-enableavalanche=1', '-avacooldown=0']] * self.num_nodes def 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 wait_until(lambda: all(proof_inv_found(i) for i in node.p2ps)) self.log.info("Test that we don't send the same inv several times") extra_peer = ProofInvStoreP2PInterface() node.add_p2p_connection(extra_peer) # Send the same proof one more time node.sendavalancheproof(proof.serialize().hex()) # Our new extra peer should receive it but not the others wait_until(lambda: proof_inv_found(extra_peer)) assert all(p.proof_invs_counter == 1 for p in node.p2ps) # Send the proof again and force the send loop to be processed for peer in node.p2ps: node.sendavalancheproof(proof.serialize().hex()) peer.sync_with_ping() assert all(p.proof_invs_counter == 1 for p in node.p2ps) def test_receive_proof(self): self.log.info("Test a peer is created on proof reception") node = self.nodes[0] _, proof = gen_proof(node) peer = node.add_p2p_connection(P2PInterface()) msg = msg_avaproof() msg.proof = proof peer.send_message(msg) wait_until(lambda: proof.proofid in get_proof_ids(node)) self.log.info("Test receiving a proof with missing utxo is orphaned") privkey = ECKey() privkey.generate() orphan_hex = node.buildavalancheproof( 42, 2000000000, privkey.get_pubkey().get_bytes().hex(), [{ 'txid': '0' * 64, 'vout': 0, 'amount': 10e6, 'height': 42, 'iscoinbase': False, 'privatekey': bytes_to_wif(privkey.get_bytes()), }] ) orphan = FromHex(AvalancheProof(), orphan_hex) orphan_proofid = "{:064x}".format(orphan.proofid) msg = msg_avaproof() msg.proof = orphan peer.send_message(msg) wait_for_proof(node, orphan_proofid, expect_orphan=True) def test_ban_invalid_proof(self): node = self.nodes[0] _, bad_proof = 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(privkey) + "-avamasterkey={}".format(bytes_to_wif(privkey.get_bytes())) ]) # Connect a block to make the proof be added to our pool node.generate(1) wait_until(lambda: proof.proofid in get_proof_ids(node)) [connect_nodes(node, n) for n in nodes[:i]] return proofids proofids = restart_nodes_with_proof(self.nodes) self.log.info("Nodes should eventually get the proof from their peer") self.sync_proofs() for node in self.nodes: assert_equal(set(get_proof_ids(node)), proofids) def test_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) wait_until(lambda: proof_inv_received(peers)) # If no peer request the proof for download, the node should reattempt # broadcasting to all new peers after 10 to 15 minutes. peers = add_peers(3) node.mockscheduler(MAX_INITIAL_BROADCAST_DELAY + 1) peers[-1].sync_with_ping() wait_until(lambda: proof_inv_received(peers)) # If at least one peer requests the proof, there is no more attempt to # broadcast it node.setmocktime(int(time.time()) + UNCONDITIONAL_RELAY_DELAY) msg = msg_getdata([CInv(t=MSG_AVA_PROOF, h=proof.proofid)]) peers[-1].send_message(msg) # Give enough time for the node to broadcast the proof again peers = add_peers(3) node.mockscheduler(MAX_INITIAL_BROADCAST_DELAY + 1) peers[-1].sync_with_ping() assert not proof_inv_received(peers) self.log.info( "Proofs that become invalid should no longer be broadcasted") # Restart and add connect a new set of peers self.restart_node(0) # Broadcast the proof peers = add_peers(3) assert node.sendavalancheproof(proof.serialize().hex()) wait_until(lambda: proof_inv_received(peers)) # Sanity check our node knows the proof, and it is valid wait_for_proof(node, proofid_hex, expect_orphan=False) # Mature the utxo then spend it node.generate(100) utxo = proof.stakes[0].stake.utxo raw_tx = node.createrawtransaction( inputs=[{ # coinbase "txid": "{:064x}".format(utxo.hash), "vout": utxo.n }], outputs={ADDRESS_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 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/test_framework/avatools.py b/test/functional/test_framework/avatools.py index 0e686e10a..830ba691d 100644 --- a/test/functional/test_framework/avatools.py +++ b/test/functional/test_framework/avatools.py @@ -1,279 +1,277 @@ #!/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, Optional, List, Dict from .authproxy import JSONRPCException from .key import ECKey from .messages import ( AvalancheDelegation, AvalancheProof, AvalancheResponse, CInv, CTransaction, FromHex, hash256, msg_avahello, msg_avapoll, msg_tcpavaresponse, NODE_AVALANCHE, NODE_NETWORK, TCPAvalancheResponse, ToHex, ) from .p2p import P2PInterface, p2p_lock from .test_node import TestNode from .util import ( assert_equal, satoshi_round, wait_until, ) -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'] ).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(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): 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(" 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() pubkey = privkey.get_pubkey() stakes = create_coinbase_stakes( node, blockhashes, node.get_deterministic_priv_key().key) proof_hex = node.buildavalancheproof( 42, 2000000000, pubkey.get_bytes().hex(), stakes) - return bytes_to_wif(privkey.get_bytes()), FromHex( - AvalancheProof(), proof_hex) + return privkey, FromHex(AvalancheProof(), proof_hex)