|
| 1 | +#!/usr/bin/env python3 |
| 2 | +# Copyright (c) 2019 The Bitcoin Core developers |
| 3 | +# Distributed under the MIT software license, see the accompanying |
| 4 | +# file COPYING or http://www.opensource.org/licenses/mit-license.php. |
| 5 | +""" |
| 6 | +Test transaction download behavior |
| 7 | +""" |
| 8 | + |
| 9 | +from test_framework.messages import ( |
| 10 | + CInv, |
| 11 | + CTransaction, |
| 12 | + FromHex, |
| 13 | + MSG_TX, |
| 14 | + MSG_TYPE_MASK, |
| 15 | + msg_inv, |
| 16 | + msg_notfound, |
| 17 | +) |
| 18 | +from test_framework.mininode import ( |
| 19 | + P2PInterface, |
| 20 | + mininode_lock, |
| 21 | +) |
| 22 | +from test_framework.test_framework import BitcoinTestFramework |
| 23 | +from test_framework.util import ( |
| 24 | + assert_equal, |
| 25 | + wait_until, |
| 26 | +) |
| 27 | +from test_framework.address import ADDRESS_BCRT1_UNSPENDABLE |
| 28 | + |
| 29 | +import time |
| 30 | + |
| 31 | + |
| 32 | +class TestP2PConn(P2PInterface): |
| 33 | + def __init__(self): |
| 34 | + super().__init__() |
| 35 | + self.tx_getdata_count = 0 |
| 36 | + |
| 37 | + def on_getdata(self, message): |
| 38 | + for i in message.inv: |
| 39 | + if i.type & MSG_TYPE_MASK == MSG_TX: |
| 40 | + self.tx_getdata_count += 1 |
| 41 | + |
| 42 | + |
| 43 | +# Constants from net_processing |
| 44 | +GETDATA_TX_INTERVAL = 60 # seconds |
| 45 | +MAX_GETDATA_RANDOM_DELAY = 2 # seconds |
| 46 | +INBOUND_PEER_TX_DELAY = 2 # seconds |
| 47 | +MAX_GETDATA_IN_FLIGHT = 100 |
| 48 | +TX_EXPIRY_INTERVAL = GETDATA_TX_INTERVAL * 10 |
| 49 | + |
| 50 | +# Python test constants |
| 51 | +NUM_INBOUND = 10 |
| 52 | +MAX_GETDATA_INBOUND_WAIT = GETDATA_TX_INTERVAL + MAX_GETDATA_RANDOM_DELAY + INBOUND_PEER_TX_DELAY |
| 53 | + |
| 54 | + |
| 55 | +class TxDownloadTest(BitcoinTestFramework): |
| 56 | + def set_test_params(self): |
| 57 | + self.setup_clean_chain = False |
| 58 | + self.num_nodes = 2 |
| 59 | + |
| 60 | + def test_tx_requests(self): |
| 61 | + self.log.info("Test that we request transactions from all our peers, eventually") |
| 62 | + |
| 63 | + txid = 0xdeadbeef |
| 64 | + |
| 65 | + self.log.info("Announce the txid from each incoming peer to node 0") |
| 66 | + msg = msg_inv([CInv(t=1, h=txid)]) |
| 67 | + for p in self.nodes[0].p2ps: |
| 68 | + p.send_message(msg) |
| 69 | + p.sync_with_ping() |
| 70 | + |
| 71 | + outstanding_peer_index = [i for i in range(len(self.nodes[0].p2ps))] |
| 72 | + |
| 73 | + def getdata_found(peer_index): |
| 74 | + p = self.nodes[0].p2ps[peer_index] |
| 75 | + with mininode_lock: |
| 76 | + return p.last_message.get("getdata") and p.last_message["getdata"].inv[-1].hash == txid |
| 77 | + |
| 78 | + node_0_mocktime = int(time.time()) |
| 79 | + while outstanding_peer_index: |
| 80 | + node_0_mocktime += MAX_GETDATA_INBOUND_WAIT |
| 81 | + self.nodes[0].setmocktime(node_0_mocktime) |
| 82 | + wait_until(lambda: any(getdata_found(i) for i in outstanding_peer_index)) |
| 83 | + for i in outstanding_peer_index: |
| 84 | + if getdata_found(i): |
| 85 | + outstanding_peer_index.remove(i) |
| 86 | + |
| 87 | + self.nodes[0].setmocktime(0) |
| 88 | + self.log.info("All outstanding peers received a getdata") |
| 89 | + |
| 90 | + def test_inv_block(self): |
| 91 | + self.log.info("Generate a transaction on node 0") |
| 92 | + tx = self.nodes[0].createrawtransaction( |
| 93 | + inputs=[{ # coinbase |
| 94 | + "txid": self.nodes[0].getblock(self.nodes[0].getblockhash(1))['tx'][0], |
| 95 | + "vout": 0 |
| 96 | + }], |
| 97 | + outputs={ADDRESS_BCRT1_UNSPENDABLE: 50 - 0.00025}, |
| 98 | + ) |
| 99 | + tx = self.nodes[0].signrawtransactionwithkey( |
| 100 | + hexstring=tx, |
| 101 | + privkeys=[self.nodes[0].get_deterministic_priv_key().key], |
| 102 | + )['hex'] |
| 103 | + ctx = FromHex(CTransaction(), tx) |
| 104 | + txid = int(ctx.rehash(), 16) |
| 105 | + |
| 106 | + self.log.info( |
| 107 | + "Announce the transaction to all nodes from all {} incoming peers, but never send it".format(NUM_INBOUND)) |
| 108 | + msg = msg_inv([CInv(t=1, h=txid)]) |
| 109 | + for p in self.peers: |
| 110 | + p.send_message(msg) |
| 111 | + p.sync_with_ping() |
| 112 | + |
| 113 | + self.log.info("Put the tx in node 0's mempool") |
| 114 | + self.nodes[0].sendrawtransaction(tx) |
| 115 | + |
| 116 | + # Since node 1 is connected outbound to an honest peer (node 0), it |
| 117 | + # should get the tx within a timeout. (Assuming that node 0 |
| 118 | + # announced the tx within the timeout) |
| 119 | + # The timeout is the sum of |
| 120 | + # * the worst case until the tx is first requested from an inbound |
| 121 | + # peer, plus |
| 122 | + # * the first time it is re-requested from the outbound peer, plus |
| 123 | + # * 2 seconds to avoid races |
| 124 | + timeout = 2 + (MAX_GETDATA_RANDOM_DELAY + INBOUND_PEER_TX_DELAY) + ( |
| 125 | + GETDATA_TX_INTERVAL + MAX_GETDATA_RANDOM_DELAY) |
| 126 | + self.log.info("Tx should be received at node 1 after {} seconds".format(timeout)) |
| 127 | + self.sync_mempools(timeout=timeout) |
| 128 | + |
| 129 | + def test_in_flight_max(self): |
| 130 | + self.log.info("Test that we don't request more than {} transactions from any peer, every {} minutes".format( |
| 131 | + MAX_GETDATA_IN_FLIGHT, TX_EXPIRY_INTERVAL / 60)) |
| 132 | + txids = [i for i in range(MAX_GETDATA_IN_FLIGHT + 2)] |
| 133 | + |
| 134 | + p = self.nodes[0].p2ps[0] |
| 135 | + |
| 136 | + with mininode_lock: |
| 137 | + p.tx_getdata_count = 0 |
| 138 | + |
| 139 | + p.send_message(msg_inv([CInv(t=1, h=i) for i in txids])) |
| 140 | + wait_until(lambda: p.tx_getdata_count >= MAX_GETDATA_IN_FLIGHT, lock=mininode_lock) |
| 141 | + with mininode_lock: |
| 142 | + assert_equal(p.tx_getdata_count, MAX_GETDATA_IN_FLIGHT) |
| 143 | + |
| 144 | + self.log.info("Now check that if we send a NOTFOUND for a transaction, we'll get one more request") |
| 145 | + p.send_message(msg_notfound(vec=[CInv(t=1, h=txids[0])])) |
| 146 | + wait_until(lambda: p.tx_getdata_count >= MAX_GETDATA_IN_FLIGHT + 1, timeout=10, lock=mininode_lock) |
| 147 | + with mininode_lock: |
| 148 | + assert_equal(p.tx_getdata_count, MAX_GETDATA_IN_FLIGHT + 1) |
| 149 | + |
| 150 | + WAIT_TIME = TX_EXPIRY_INTERVAL // 2 + TX_EXPIRY_INTERVAL |
| 151 | + self.log.info("if we wait about {} minutes, we should eventually get more requests".format(WAIT_TIME / 60)) |
| 152 | + self.nodes[0].setmocktime(int(time.time() + WAIT_TIME)) |
| 153 | + wait_until(lambda: p.tx_getdata_count == MAX_GETDATA_IN_FLIGHT + 2) |
| 154 | + self.nodes[0].setmocktime(0) |
| 155 | + |
| 156 | + def run_test(self): |
| 157 | + # Setup the p2p connections |
| 158 | + self.peers = [] |
| 159 | + for node in self.nodes: |
| 160 | + for i in range(NUM_INBOUND): |
| 161 | + self.peers.append(node.add_p2p_connection(TestP2PConn())) |
| 162 | + |
| 163 | + self.log.info("Nodes are setup with {} incoming connections each".format(NUM_INBOUND)) |
| 164 | + |
| 165 | + # Test the in-flight max first, because we want no transactions in |
| 166 | + # flight ahead of this test. |
| 167 | + self.test_in_flight_max() |
| 168 | + |
| 169 | + self.test_inv_block() |
| 170 | + |
| 171 | + self.test_tx_requests() |
| 172 | + |
| 173 | + |
| 174 | +if __name__ == '__main__': |
| 175 | + TxDownloadTest().main() |
0 commit comments