diff --git a/test/functional/feature_block.py b/test/functional/feature_block.py --- a/test/functional/feature_block.py +++ b/test/functional/feature_block.py @@ -90,10 +90,7 @@ def run_test(self): node = self.nodes[0] # convenience reference to the node - # reconnect_p2p() expects the network thread to be running - network_thread_start() - - self.reconnect_p2p() + self.bootstrap_p2p() # Add one p2p connection to the node self.block_heights = {} self.coinbase_key = CECKey() @@ -1376,14 +1373,10 @@ self.blocks[block_number] = block return block - def reconnect_p2p(self): + def bootstrap_p2p(self): """Add a P2P connection to the node. - The node gets disconnected several times in this test. This helper - method reconnects the p2p and restarts the network thread.""" - - network_thread_join() - self.nodes[0].disconnect_p2ps() + Helper to connect and wait for version handshake.""" self.nodes[0].add_p2p_connection(P2PDataStore()) network_thread_start() # We need to wait for the initial getheaders from the peer before we @@ -1394,6 +1387,15 @@ # unexpectedly disconnected if the DoS score for that error is 50. self.nodes[0].p2p.wait_for_getheaders(timeout=5) + def reconnect_p2p(self): + """Tear down and bootstrap the P2P connection to the node. + + The node gets disconnected several times in this test. This helper + method reconnects the p2p and restarts the network thread.""" + self.nodes[0].disconnect_p2ps() + network_thread_join() + self.bootstrap_p2p() + def sync_blocks(self, blocks, success=True, reject_code=None, reject_reason=None, request_block=True, reconnect=False, timeout=60): """Sends blocks to test node. Syncs and verifies that tip has advanced to most recent block. diff --git a/test/functional/p2p_invalid_tx.py b/test/functional/p2p_invalid_tx.py --- a/test/functional/p2p_invalid_tx.py +++ b/test/functional/p2p_invalid_tx.py @@ -12,9 +12,20 @@ create_coinbase, create_transaction, ) -from test_framework.messages import COIN -from test_framework.mininode import network_thread_start, P2PDataStore +from test_framework.txtools import pad_tx +from test_framework.messages import ( + COIN, + COutPoint, + CTransaction, + CTxIn, + CTxOut, +) +from test_framework.mininode import network_thread_start, P2PDataStore, network_thread_join from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import ( + assert_equal, + wait_until, +) class InvalidTxRequestTest(BitcoinTestFramework): @@ -22,16 +33,29 @@ def set_test_params(self): self.num_nodes = 1 self.setup_clean_chain = True - self.extra_args = [["-whitelist=127.0.0.1"]] - def run_test(self): - # Add p2p connection to node0 - # convenience reference to the node - node = self.nodes[0] - node.add_p2p_connection(P2PDataStore()) + def bootstrap_p2p(self, *, num_connections=1): + """Add a P2P connection to the node. + Helper to connect and wait for version handshake.""" + for _ in range(num_connections): + self.nodes[0].add_p2p_connection(P2PDataStore()) network_thread_start() - node.p2p.wait_for_verack() + self.nodes[0].p2p.wait_for_verack() + + def reconnect_p2p(self, **kwargs): + """Tear down and bootstrap the P2P connection to the node. + + The node gets disconnected several times in this test. This helper + method reconnects the p2p and restarts the network thread.""" + self.nodes[0].disconnect_p2ps() + network_thread_join() + self.bootstrap_p2p(**kwargs) + + def run_test(self): + node = self.nodes[0] # convenience reference to the node + + self.bootstrap_p2p() # Add one p2p connection to the node best_block = self.nodes[0].getbestblockhash() tip = int(best_block, 16) @@ -54,11 +78,93 @@ # b'\x64' is OP_NOTIF # Transaction will be rejected with code 16 (REJECT_INVALID) + # and we get disconnected immediately + self.log.info('Test a transaction that is rejected') tx1 = create_transaction(block1.vtx[0], 0, b'\x64', 50 * COIN - 12000) - node.p2p.send_txs_and_test([tx1], node, success=False, reject_code=16, - reject_reason=b'mandatory-script-verify-flag-failed (Only push operators allowed in signature scripts)') + node.p2p.send_txs_and_test( + [tx1], node, success=False, expect_disconnect=True) + + # Make two p2p connections to provide the node with orphans + # * p2ps[0] will send valid orphan txs (one with low fee) + # * p2ps[1] will send an invalid orphan tx (and is later disconnected for that) + self.reconnect_p2p(num_connections=2) + + self.log.info('Test orphan transaction handling ... ') + # Create a root transaction that we withold until all dependend transactions + # are sent out and in the orphan cache + tx_withhold = CTransaction() + tx_withhold.vin.append( + CTxIn(outpoint=COutPoint(block1.vtx[0].sha256, 0))) + tx_withhold.vout.append( + CTxOut(nValue=50 * COIN - 12000, scriptPubKey=b'\x51')) + pad_tx(tx_withhold) + tx_withhold.calc_sha256() + + # Our first orphan tx with some outputs to create further orphan txs + tx_orphan_1 = CTransaction() + tx_orphan_1.vin.append( + CTxIn(outpoint=COutPoint(tx_withhold.sha256, 0))) + tx_orphan_1.vout = [CTxOut(nValue=10 * COIN, scriptPubKey=b'\x51')] * 3 + pad_tx(tx_orphan_1) + tx_orphan_1.calc_sha256() + + # A valid transaction with low fee + tx_orphan_2_no_fee = CTransaction() + tx_orphan_2_no_fee.vin.append( + CTxIn(outpoint=COutPoint(tx_orphan_1.sha256, 0))) + tx_orphan_2_no_fee.vout.append( + CTxOut(nValue=10 * COIN, scriptPubKey=b'\x51')) + pad_tx(tx_orphan_2_no_fee) + + # A valid transaction with sufficient fee + tx_orphan_2_valid = CTransaction() + tx_orphan_2_valid.vin.append( + CTxIn(outpoint=COutPoint(tx_orphan_1.sha256, 1))) + tx_orphan_2_valid.vout.append( + CTxOut(nValue=10 * COIN - 12000, scriptPubKey=b'\x51')) + tx_orphan_2_valid.calc_sha256() + pad_tx(tx_orphan_2_valid) + + # An invalid transaction with negative fee + tx_orphan_2_invalid = CTransaction() + tx_orphan_2_invalid.vin.append( + CTxIn(outpoint=COutPoint(tx_orphan_1.sha256, 2))) + tx_orphan_2_invalid.vout.append( + CTxOut(nValue=11 * COIN, scriptPubKey=b'\x51')) + pad_tx(tx_orphan_2_invalid) + + self.log.info('Send the orphans ... ') + # Send valid orphan txs from p2ps[0] + node.p2p.send_txs_and_test( + [tx_orphan_1, tx_orphan_2_no_fee, tx_orphan_2_valid], node, success=False) + # Send invalid tx from p2ps[1] + node.p2ps[1].send_txs_and_test( + [tx_orphan_2_invalid], node, success=False) + + # Mempool should be empty + assert_equal(0, node.getmempoolinfo()['size']) + assert_equal(2, len(node.getpeerinfo())) # p2ps[1] is still connected + + self.log.info('Send the withhold tx ... ') + node.p2p.send_txs_and_test([tx_withhold], node, success=True) + + # Transactions that should end up in the mempool + expected_mempool = { + t.hash + for t in [ + tx_withhold, # The transaction that is the root for all orphans + tx_orphan_1, # The orphan transaction that splits the coins + # The valid transaction (with sufficient fee) + tx_orphan_2_valid, + ] + } + # Transactions that do not end up in the mempool + # tx_orphan_no_fee, because it has too low fee (p2ps[0] is not disconnected for relaying that tx) + # tx_orphan_invaid, because it has negative fee (p2ps[1] is disconnected for relaying that tx) - # TODO: test further transactions... + # p2ps[1] is no longer connected + wait_until(lambda: 1 == len(node.getpeerinfo()), timeout=12) + assert_equal(expected_mempool, set(node.getrawmempool())) if __name__ == '__main__': diff --git a/test/functional/test_framework/mininode.py b/test/functional/test_framework/mininode.py --- a/test/functional/test_framework/mininode.py +++ b/test/functional/test_framework/mininode.py @@ -648,13 +648,13 @@ wait_until(lambda: self.reject_reason_received == reject_reason, lock=mininode_lock) - def send_txs_and_test(self, txs, rpc, success=True, reject_code=None, reject_reason=None): + def send_txs_and_test(self, txs, rpc, success=True, expect_disconnect=False, reject_code=None, reject_reason=None): """Send txs to test node and test whether they're accepted to the mempool. - add all txs to our tx_store - send tx messages for all txs - - if success is True: assert that the tx is accepted to the mempool - - if success is False: assert that the tx is not accepted to the mempool + - if success is True/False: assert that the txs are/are not accepted to the mempool + - if expect_disconnect is True: Skip the sync with ping - if reject_code and reject_reason are set: assert that the correct reject message is received.""" with mininode_lock: @@ -667,7 +667,10 @@ for tx in txs: self.send_message(msg_tx(tx)) - self.sync_with_ping() + if expect_disconnect: + self.wait_for_disconnect() + else: + self.sync_with_ping() raw_mempool = rpc.getrawmempool() if success: