From fadfbd33ab4ea57f1efa2d10a346c25c4ad18b58 Mon Sep 17 00:00:00 2001 From: MarcoFalke Date: Tue, 10 Apr 2018 18:08:01 -0400 Subject: [PATCH] qa: Add test for orphan handling --- test/functional/feature_block.py | 22 +++-- test/functional/p2p_invalid_tx.py | 109 ++++++++++++++++++--- test/functional/test_framework/mininode.py | 11 ++- 3 files changed, 116 insertions(+), 26 deletions(-) diff --git a/test/functional/feature_block.py b/test/functional/feature_block.py index 181c7f3369..5959a8a541 100755 --- a/test/functional/feature_block.py +++ b/test/functional/feature_block.py @@ -82,10 +82,7 @@ class FullBlockTest(BitcoinTestFramework): 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() @@ -1296,18 +1293,23 @@ class FullBlockTest(BitcoinTestFramework): 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() self.nodes[0].p2p.wait_for_verack() + 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 index 69ce529ad6..8a0961be1f 100755 --- a/test/functional/p2p_invalid_tx.py +++ b/test/functional/p2p_invalid_tx.py @@ -6,24 +6,48 @@ In this test we connect to one node over p2p, and test tx requests.""" from test_framework.blocktools import create_block, create_coinbase, create_transaction -from test_framework.messages import COIN -from test_framework.mininode import network_thread_start, P2PDataStore +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): - def set_test_params(self): self.num_nodes = 1 self.setup_clean_chain = True - self.extra_args = [["-whitelist=127.0.0.1"]] + + 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() + 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): - # Add p2p connection to node0 node = self.nodes[0] # convenience reference to the node - node.add_p2p_connection(P2PDataStore()) - network_thread_start() - node.p2p.wait_for_verack() + self.bootstrap_p2p() # Add one p2p connection to the node best_block = self.nodes[0].getbestblockhash() tip = int(best_block, 16) @@ -44,12 +68,73 @@ class InvalidTxRequestTest(BitcoinTestFramework): # 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 (Invalid OP_IF construction)') + node.p2p.send_txs_and_test([tx1], node, success=False, expect_disconnect=True) - # Verify valid transaction - tx1 = create_transaction(block1.vtx[0], 0, b'', 50 * COIN - 12000) - node.p2p.send_txs_and_test([tx1], node, success=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')) + 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 + 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')) + + # 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() + + # 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')) + + 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) + + assert_equal(0, node.getmempoolinfo()['size']) # Mempool should be empty + 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 + tx_orphan_2_valid, # The valid transaction (with sufficient fee) + ] + } + # 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) + + wait_until(lambda: 1 == len(node.getpeerinfo()), timeout=12) # p2ps[1] is no longer connected + 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 index aba2841682..0fbb3ce2cd 100755 --- a/test/functional/test_framework/mininode.py +++ b/test/functional/test_framework/mininode.py @@ -554,13 +554,13 @@ class P2PDataStore(P2PInterface): if reject_reason is not None: 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: assert that the txs are 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: @@ -573,7 +573,10 @@ class P2PDataStore(P2PInterface): 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: