From 2edec4fe68c5fe0fefb4c0561e8353462bb439bc Mon Sep 17 00:00:00 2001 From: Suhas Daftuar Date: Mon, 4 May 2015 10:50:24 -0400 Subject: [PATCH] P2P regression test for new AcceptBlock behavior Github-Pull: #5875 Rebased-From: aa8c827968a68a3adc4df5f126635e37a71a7fbb --- qa/pull-tester/rpc-tests.sh | 1 + qa/rpc-tests/p2p-acceptblock.py | 226 ++++++++++++++++++++++++++++++++ 2 files changed, 227 insertions(+) create mode 100755 qa/rpc-tests/p2p-acceptblock.py diff --git a/qa/pull-tester/rpc-tests.sh b/qa/pull-tester/rpc-tests.sh index 88772a9c32b23..9b318650e9394 100755 --- a/qa/pull-tester/rpc-tests.sh +++ b/qa/pull-tester/rpc-tests.sh @@ -51,6 +51,7 @@ testScriptsExt=( 'invalidblockrequest.py' 'rawtransactions.py' # 'forknotify.py' + 'p2p-acceptblock.py' ); extArg="-extended" diff --git a/qa/rpc-tests/p2p-acceptblock.py b/qa/rpc-tests/p2p-acceptblock.py new file mode 100755 index 0000000000000..fcdd1e1b99c88 --- /dev/null +++ b/qa/rpc-tests/p2p-acceptblock.py @@ -0,0 +1,226 @@ +#!/usr/bin/env python2 +# +# Distributed under the MIT/X11 software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +# + +from test_framework.mininode import * +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import * +import time +from test_framework.blocktools import create_block, create_coinbase + +''' +AcceptBlockTest -- test processing of unrequested blocks. + +Since behavior differs when receiving unrequested blocks from whitelisted peers +versus non-whitelisted peers, this tests the behavior of both (effectively two +separate tests running in parallel). + +Setup: two nodes, node0 and node1, not connected to each other. Node0 does not +whitelist localhost, but node1 does. They will each be on their own chain for +this test. + +We have one NodeConn connection to each, test_node and white_node respectively. + +The test: +1. Generate one block on each node, to leave IBD. + +2. Mine a new block on each tip, and deliver to each node from node's peer. + The tip should advance. + +3. Mine a block that forks the previous block, and deliver to each node from + corresponding peer. + Node0 should not process this block (just accept the header), because it is + unrequested and doesn't have more work than the tip. + Node1 should process because this is coming from a whitelisted peer. + +4. Send another block that builds on the forking block. + Node0 should process this block but be stuck on the shorter chain, because + it's missing an intermediate block. + Node1 should reorg to this longer chain. + +5. Send a duplicate of the block in #3 to Node0. + Node0 should not process the block because it is unrequested, and stay on + the shorter chain. + +6. Send Node0 an inv for the height 3 block produced in #4 above. + Node0 should figure out that Node0 has the missing height 2 block and send a + getdata. + +7. Send Node0 the missing block again. + Node0 should process and the tip should advance. +''' + +# TestNode: bare-bones "peer". Used mostly as a conduit for a test to sending +# p2p messages to a node, generating the messages in the main testing logic. +class TestNode(NodeConnCB): + def __init__(self): + NodeConnCB.__init__(self) + self.create_callback_map() + self.connection = None + + def add_connection(self, conn): + self.connection = conn + + # Track the last getdata message we receive (used in the test) + def on_getdata(self, conn, message): + self.last_getdata = message + + # Spin until verack message is received from the node. + # We use this to signal that our test can begin. This + # is called from the testing thread, so it needs to acquire + # the global lock. + def wait_for_verack(self): + while True: + with mininode_lock: + if self.verack_received: + return + time.sleep(0.05) + + # Wrapper for the NodeConn's send_message function + def send_message(self, message): + self.connection.send_message(message) + +class AcceptBlockTest(BitcoinTestFramework): + def add_options(self, parser): + parser.add_option("--testbinary", dest="testbinary", + default=os.getenv("BITCOIND", "bitcoind"), + help="bitcoind binary to test") + + def setup_chain(self): + initialize_chain_clean(self.options.tmpdir, 2) + + def setup_network(self): + # Node0 will be used to test behavior of processing unrequested blocks + # from peers which are not whitelisted, while Node1 will be used for + # the whitelisted case. + self.nodes = [] + self.nodes.append(start_node(0, self.options.tmpdir, ["-debug"], + binary=self.options.testbinary)) + self.nodes.append(start_node(1, self.options.tmpdir, + ["-debug", "-whitelist=127.0.0.1"], + binary=self.options.testbinary)) + + def run_test(self): + # Setup the p2p connections and start up the network thread. + test_node = TestNode() # connects to node0 (not whitelisted) + white_node = TestNode() # connects to node1 (whitelisted) + + connections = [] + connections.append(NodeConn('127.0.0.1', p2p_port(0), self.nodes[0], test_node)) + connections.append(NodeConn('127.0.0.1', p2p_port(1), self.nodes[1], white_node)) + test_node.add_connection(connections[0]) + white_node.add_connection(connections[1]) + + NetworkThread().start() # Start up network handling in another thread + + # Test logic begins here + test_node.wait_for_verack() + white_node.wait_for_verack() + + # 1. Have both nodes mine a block (leave IBD) + [ n.generate(1) for n in self.nodes ] + tips = [ int ("0x" + n.getbestblockhash() + "L", 0) for n in self.nodes ] + + # 2. Send one block that builds on each tip. + # This should be accepted. + blocks_h2 = [] # the height 2 blocks on each node's chain + for i in xrange(2): + blocks_h2.append(create_block(tips[i], create_coinbase(), time.time()+1)) + blocks_h2[i].solve() + test_node.send_message(msg_block(blocks_h2[0])) + white_node.send_message(msg_block(blocks_h2[1])) + + time.sleep(1) + assert_equal(self.nodes[0].getblockcount(), 2) + assert_equal(self.nodes[1].getblockcount(), 2) + print "First height 2 block accepted by both nodes" + + # 3. Send another block that builds on the original tip. + blocks_h2f = [] # Blocks at height 2 that fork off the main chain + for i in xrange(2): + blocks_h2f.append(create_block(tips[i], create_coinbase(), blocks_h2[i].nTime+1)) + blocks_h2f[i].solve() + test_node.send_message(msg_block(blocks_h2f[0])) + white_node.send_message(msg_block(blocks_h2f[1])) + + time.sleep(1) # Give time to process the block + for x in self.nodes[0].getchaintips(): + if x['hash'] == blocks_h2f[0].hash: + assert_equal(x['status'], "headers-only") + + for x in self.nodes[1].getchaintips(): + if x['hash'] == blocks_h2f[1].hash: + assert_equal(x['status'], "valid-headers") + + print "Second height 2 block accepted only from whitelisted peer" + + # 4. Now send another block that builds on the forking chain. + blocks_h3 = [] + for i in xrange(2): + blocks_h3.append(create_block(blocks_h2f[i].sha256, create_coinbase(), blocks_h2f[i].nTime+1)) + blocks_h3[i].solve() + test_node.send_message(msg_block(blocks_h3[0])) + white_node.send_message(msg_block(blocks_h3[1])) + + time.sleep(1) + # Since the earlier block was not processed by node0, the new block + # can't be fully validated. + for x in self.nodes[0].getchaintips(): + if x['hash'] == blocks_h3[0].hash: + assert_equal(x['status'], "headers-only") + + # But this block should be accepted by node0 since it has more work. + try: + self.nodes[0].getblock(blocks_h3[0].hash) + print "Unrequested more-work block accepted from non-whitelisted peer" + except: + raise AssertionError("Unrequested more work block was not processed") + + # Node1 should have accepted and reorged. + assert_equal(self.nodes[1].getblockcount(), 3) + print "Successfully reorged to length 3 chain from whitelisted peer" + + # 5. Test handling of unrequested block on the node that didn't process + # Should still not be processed (even though it has a child that has more + # work). + test_node.send_message(msg_block(blocks_h2f[0])) + + # Here, if the sleep is too short, the test could falsely succeed (if the + # node hasn't processed the block by the time the sleep returns, and then + # the node processes it and incorrectly advances the tip). + # But this would be caught later on, when we verify that an inv triggers + # a getdata request for this block. + time.sleep(1) + assert_equal(self.nodes[0].getblockcount(), 2) + print "Unrequested block that would complete more-work chain was ignored" + + # 6. Try to get node to request the missing block. + # Poke the node with an inv for block at height 3 and see if that + # triggers a getdata on block 2 (it should if block 2 is missing). + with mininode_lock: + # Clear state so we can check the getdata request + test_node.last_getdata = None + test_node.send_message(msg_inv([CInv(2, blocks_h3[0].sha256)])) + + time.sleep(1) + with mininode_lock: + getdata = test_node.last_getdata + + # Check that the getdata is for the right block + assert_equal(len(getdata.inv), 1) + assert_equal(getdata.inv[0].hash, blocks_h2f[0].sha256) + print "Inv at tip triggered getdata for unprocessed block" + + # 7. Send the missing block for the third time (now it is requested) + test_node.send_message(msg_block(blocks_h2f[0])) + + time.sleep(1) + assert_equal(self.nodes[0].getblockcount(), 3) + print "Successfully reorged to length 3 chain from non-whitelisted peer" + + [ c.disconnect_node() for c in connections ] + +if __name__ == '__main__': + AcceptBlockTest().main()