Skip to content

Commit

Permalink
Merge pull request #2458 from patricklodder/1.14.4-disconnect-bad-nod…
Browse files Browse the repository at this point in the history
…e-test

Test that peers building on invalid blocks get disconnected
  • Loading branch information
Ross Nicoll committed Aug 17, 2021
2 parents b8a29b9 + caf26b7 commit 59da28c
Show file tree
Hide file tree
Showing 2 changed files with 100 additions and 2 deletions.
98 changes: 96 additions & 2 deletions qa/rpc-tests/p2p-acceptblock.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import *
import time
from test_framework.blocktools import create_block, create_coinbase
from test_framework.blocktools import create_block, create_coinbase, create_transaction

'''
AcceptBlockTest -- test processing of unrequested blocks.
Expand Down Expand Up @@ -54,18 +54,27 @@
7. Send Node0 the missing block again.
Node0 should process and the tip should advance.
8. Create a fork which is invalid at a height longer than the current chain
(ie to which the node will try to reorg) but which has headers built on top
of the invalid block. Check that we get disconnected if we send more headers
on the chain the node now knows to be invalid.
9. Test Node1 is able to sync when connected to node0 (which should have sufficient
work on its chain).
'''

# 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):
def __init__(self, timeout_factor=1):
NodeConnCB.__init__(self)
self.connection = None
self.ping_counter = 1
self.last_pong = msg_pong()

def add_connection(self, conn):
self.has_been_disconnected = False
self.connection = conn

# Track the last getdata message we receive (used in the test)
Expand Down Expand Up @@ -104,6 +113,17 @@ def sync_with_ping(self, timeout=30):
self.ping_counter += 1
return received_pong

# wait for the socket to be in a closed state
def wait_for_disconnect(self, timeout=60):
if self.connection == None:
return True
sleep_time = 0.05
is_closed = self.connection.state == "closed"
while not is_closed and timeout > 0:
time.sleep(sleep_time)
timeout -= sleep_time
is_closed = self.connection.state == "closed"
return is_closed

class AcceptBlockTest(BitcoinTestFramework):
def add_options(self, parser):
Expand Down Expand Up @@ -271,8 +291,82 @@ def run_test(self):

test_node.sync_with_ping()
assert_equal(self.nodes[0].getblockcount(), 1442)
self.nodes[0].getblock(all_blocks[1438].hash)
assert_equal(self.nodes[0].getbestblockhash(), all_blocks[1438].hash)
assert_raises_jsonrpc(-1, "Block not found on disk", self.nodes[0].getblock, all_blocks[1439].hash)
print("Successfully reorged to longer chain from non-whitelisted peer")

# 8. Create a chain which is invalid at a height longer than the
# current chain, but which has more blocks on top of that
block_1441f = create_block(all_blocks[1436].sha256, create_coinbase(1441), all_blocks[1436].nTime+1)
block_1441f.solve()
block_1442f = create_block(block_1441f.sha256, create_coinbase(1440), block_1441f.nTime+1)
block_1442f.solve()
block_1443 = create_block(block_1442f.sha256, create_coinbase(1441), block_1442f.nTime+1)
# block_1443 spends a coinbase below maturity!
block_1443.vtx.append(create_transaction(block_1442f.vtx[0], 0, b"42", 1))
block_1443.hashMerkleRoot = block_1443.calc_merkle_root()
block_1443.solve()
block_1444 = create_block(block_1443.sha256, create_coinbase(1444), block_1443.nTime+1)
block_1444.solve()

# Now send all the headers on the chain and enough blocks to trigger reorg
headers_message = msg_headers()
headers_message.headers.append(CBlockHeader(block_1441f))
headers_message.headers.append(CBlockHeader(block_1442f))
headers_message.headers.append(CBlockHeader(block_1443))
headers_message.headers.append(CBlockHeader(block_1444))
test_node.send_message(headers_message)

test_node.sync_with_ping()
tip_entry_found = False
for x in self.nodes[0].getchaintips():
if x['hash'] == block_1444.hash:
assert_equal(x['status'], "headers-only")
tip_entry_found = True
assert(tip_entry_found)
assert_raises_jsonrpc(-1, "Block not found on disk", self.nodes[0].getblock, block_1444.hash)

test_node.send_message(msg_block(block_1441f))
test_node.send_message(msg_block(block_1442f))

test_node.sync_with_ping()
self.nodes[0].getblock(block_1441f.hash)
self.nodes[0].getblock(block_1442f.hash)

test_node.send_message(msg_block(block_1443))

# At this point we've sent an obviously-bogus block,
# and we must get disconnected
assert_equal(test_node.wait_for_disconnect(), True)
print("Successfully got disconnected after sending an invalid block")

# recreate our malicious node
test_node = TestNode() # connects to node (not whitelisted)
connections[0] = NodeConn('127.0.0.1', p2p_port(0), self.nodes[0], test_node)
test_node.add_connection(connections[0])
test_node.wait_for_verack()

# We should have failed reorg and switched back to 1442 (but have block 1443)
assert_equal(self.nodes[0].getblockcount(), 1442)
assert_equal(self.nodes[0].getbestblockhash(), all_blocks[1438].hash)
assert_equal(self.nodes[0].getblock(block_1443.hash)["confirmations"], -1)

# Now send a new header on the invalid chain, indicating we're forked
# off, and expect to get disconnected
block_1445 = create_block(block_1444.sha256, create_coinbase(1445), block_1444.nTime+1)
block_1445.solve()
headers_message = msg_headers()
headers_message.headers.append(CBlockHeader(block_1445))
test_node.send_message(headers_message)
assert_equal(test_node.wait_for_disconnect(), True)
print("Successfully got disconnected after building on an invalid block")

# 9. Connect node1 to node0 and ensure it is able to sync
connect_nodes(self.nodes[0], 1)
sync_blocks([self.nodes[0], self.nodes[1]])
print("Successfully synced nodes 1 and 0")

[ c.disconnect_node() for c in connections ]

if __name__ == '__main__':
Expand Down
4 changes: 4 additions & 0 deletions qa/rpc-tests/test_framework/mininode.py
Original file line number Diff line number Diff line change
Expand Up @@ -1684,6 +1684,10 @@ def handle_read(self):
if len(t) > 0:
self.recvbuf += t
self.got_data()
else:
self.show_debug_msg("MiniNode: Closing connection to %s:%d after peer disconnect..."
% (self.dstaddr, self.dstport))
self.handle_close()
except:
pass

Expand Down

0 comments on commit 59da28c

Please sign in to comment.