Skip to content

Commit eff4bd8

Browse files
author
Jim Posen
committed
[test] P2P functional test for certain fingerprinting protections
1 parent a2be3b6 commit eff4bd8

File tree

3 files changed

+161
-2
lines changed

3 files changed

+161
-2
lines changed

test/functional/p2p-fingerprint.py

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) 2017 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+
"""Test various fingerprinting protections.
6+
7+
If an stale block more than a month old or its header are requested by a peer,
8+
the node should pretend that it does not have it to avoid fingerprinting.
9+
"""
10+
11+
import time
12+
13+
from test_framework.blocktools import (create_block, create_coinbase)
14+
from test_framework.mininode import (
15+
CInv,
16+
NetworkThread,
17+
NodeConn,
18+
NodeConnCB,
19+
msg_headers,
20+
msg_block,
21+
msg_getdata,
22+
msg_getheaders,
23+
wait_until,
24+
)
25+
from test_framework.test_framework import BitcoinTestFramework
26+
from test_framework.util import (
27+
assert_equal,
28+
p2p_port,
29+
)
30+
31+
class P2PFingerprintTest(BitcoinTestFramework):
32+
def set_test_params(self):
33+
self.setup_clean_chain = True
34+
self.num_nodes = 1
35+
36+
# Build a chain of blocks on top of given one
37+
def build_chain(self, nblocks, prev_hash, prev_height, prev_median_time):
38+
blocks = []
39+
for _ in range(nblocks):
40+
coinbase = create_coinbase(prev_height + 1)
41+
block_time = prev_median_time + 1
42+
block = create_block(int(prev_hash, 16), coinbase, block_time)
43+
block.solve()
44+
45+
blocks.append(block)
46+
prev_hash = block.hash
47+
prev_height += 1
48+
prev_median_time = block_time
49+
return blocks
50+
51+
# Send a getdata request for a given block hash
52+
def send_block_request(self, block_hash, node):
53+
msg = msg_getdata()
54+
msg.inv.append(CInv(2, block_hash)) # 2 == "Block"
55+
node.send_message(msg)
56+
57+
# Send a getheaders request for a given single block hash
58+
def send_header_request(self, block_hash, node):
59+
msg = msg_getheaders()
60+
msg.hashstop = block_hash
61+
node.send_message(msg)
62+
63+
# Check whether last block received from node has a given hash
64+
def last_block_equals(self, expected_hash, node):
65+
block_msg = node.last_message.get("block")
66+
return block_msg and block_msg.block.rehash() == expected_hash
67+
68+
# Check whether last block header received from node has a given hash
69+
def last_header_equals(self, expected_hash, node):
70+
headers_msg = node.last_message.get("headers")
71+
return (headers_msg and
72+
headers_msg.headers and
73+
headers_msg.headers[0].rehash() == expected_hash)
74+
75+
# Checks that stale blocks timestamped more than a month ago are not served
76+
# by the node while recent stale blocks and old active chain blocks are.
77+
# This does not currently test that stale blocks timestamped within the
78+
# last month but that have over a month's worth of work are also withheld.
79+
def run_test(self):
80+
node0 = NodeConnCB()
81+
82+
connections = []
83+
connections.append(NodeConn('127.0.0.1', p2p_port(0), self.nodes[0], node0))
84+
node0.add_connection(connections[0])
85+
86+
NetworkThread().start()
87+
node0.wait_for_verack()
88+
89+
# Set node time to 60 days ago
90+
self.nodes[0].setmocktime(int(time.time()) - 60 * 24 * 60 * 60)
91+
92+
# Generating a chain of 10 blocks
93+
block_hashes = self.nodes[0].generate(nblocks=10)
94+
95+
# Create longer chain starting 2 blocks before current tip
96+
height = len(block_hashes) - 2
97+
block_hash = block_hashes[height - 1]
98+
block_time = self.nodes[0].getblockheader(block_hash)["mediantime"] + 1
99+
new_blocks = self.build_chain(5, block_hash, height, block_time)
100+
101+
# Force reorg to a longer chain
102+
node0.send_message(msg_headers(new_blocks))
103+
node0.wait_for_getdata()
104+
for block in new_blocks:
105+
node0.send_and_ping(msg_block(block))
106+
107+
# Check that reorg succeeded
108+
assert_equal(self.nodes[0].getblockcount(), 13)
109+
110+
stale_hash = int(block_hashes[-1], 16)
111+
112+
# Check that getdata request for stale block succeeds
113+
self.send_block_request(stale_hash, node0)
114+
test_function = lambda: self.last_block_equals(stale_hash, node0)
115+
wait_until(test_function, timeout=3)
116+
117+
# Check that getheader request for stale block header succeeds
118+
self.send_header_request(stale_hash, node0)
119+
test_function = lambda: self.last_header_equals(stale_hash, node0)
120+
wait_until(test_function, timeout=3)
121+
122+
# Longest chain is extended so stale is much older than chain tip
123+
self.nodes[0].setmocktime(0)
124+
tip = self.nodes[0].generate(nblocks=1)[0]
125+
assert_equal(self.nodes[0].getblockcount(), 14)
126+
127+
# Send getdata & getheaders to refresh last received getheader message
128+
block_hash = int(tip, 16)
129+
self.send_block_request(block_hash, node0)
130+
self.send_header_request(block_hash, node0)
131+
node0.sync_with_ping()
132+
133+
# Request for very old stale block should now fail
134+
self.send_block_request(stale_hash, node0)
135+
time.sleep(3)
136+
assert not self.last_block_equals(stale_hash, node0)
137+
138+
# Request for very old stale block header should now fail
139+
self.send_header_request(stale_hash, node0)
140+
time.sleep(3)
141+
assert not self.last_header_equals(stale_hash, node0)
142+
143+
# Verify we can fetch very old blocks and headers on the active chain
144+
block_hash = int(block_hashes[2], 16)
145+
self.send_block_request(block_hash, node0)
146+
self.send_header_request(block_hash, node0)
147+
node0.sync_with_ping()
148+
149+
self.send_block_request(block_hash, node0)
150+
test_function = lambda: self.last_block_equals(block_hash, node0)
151+
wait_until(test_function, timeout=3)
152+
153+
self.send_header_request(block_hash, node0)
154+
test_function = lambda: self.last_header_equals(block_hash, node0)
155+
wait_until(test_function, timeout=3)
156+
157+
if __name__ == '__main__':
158+
P2PFingerprintTest().main()

test/functional/test_framework/mininode.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1310,8 +1310,8 @@ def __repr__(self):
13101310
class msg_headers(object):
13111311
command = b"headers"
13121312

1313-
def __init__(self):
1314-
self.headers = []
1313+
def __init__(self, headers=None):
1314+
self.headers = headers if headers is not None else []
13151315

13161316
def deserialize(self, f):
13171317
# comment in bitcoind indicates these should be deserialized as blocks

test/functional/test_runner.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@
123123
'uptime.py',
124124
'resendwallettransactions.py',
125125
'minchainwork.py',
126+
'p2p-fingerprint.py',
126127
]
127128

128129
EXTENDED_SCRIPTS = [

0 commit comments

Comments
 (0)