Skip to content

Commit 3730393

Browse files
committed
Merge #8305: Improve handling of unconnecting headers
e91cf4b Add test for handling of unconnecting headers (Suhas Daftuar) 96fa953 Improve handling of unconnecting headers (Suhas Daftuar)
2 parents bc94b87 + e91cf4b commit 3730393

File tree

3 files changed

+145
-1
lines changed

3 files changed

+145
-1
lines changed

qa/rpc-tests/sendheaders.py

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,21 @@
6363
Expect: getdata request for 14 more blocks.
6464
f. Announce 1 more header that builds on that fork.
6565
Expect: no response.
66+
67+
Part 5: Test handling of headers that don't connect.
68+
a. Repeat 10 times:
69+
1. Announce a header that doesn't connect.
70+
Expect: getheaders message
71+
2. Send headers chain.
72+
Expect: getdata for the missing blocks, tip update.
73+
b. Then send 9 more headers that don't connect.
74+
Expect: getheaders message each time.
75+
c. Announce a header that does connect.
76+
Expect: no response.
77+
d. Announce 49 headers that don't connect.
78+
Expect: getheaders message each time.
79+
e. Announce one more that doesn't connect.
80+
Expect: disconnect.
6681
'''
6782

6883
class BaseNode(NodeConnCB):
@@ -77,6 +92,8 @@ def __init__(self):
7792
self.last_getdata = None
7893
self.sleep_time = 0.05
7994
self.block_announced = False
95+
self.last_getheaders = None
96+
self.disconnected = False
8097

8198
def clear_last_announcement(self):
8299
with mininode_lock:
@@ -127,6 +144,12 @@ def on_getdata(self, conn, message):
127144
def on_pong(self, conn, message):
128145
self.last_pong = message
129146

147+
def on_getheaders(self, conn, message):
148+
self.last_getheaders = message
149+
150+
def on_close(self, conn):
151+
self.disconnected = True
152+
130153
# Test whether the last announcement we received had the
131154
# right header or the right inv
132155
# inv and headers should be lists of block hashes
@@ -178,6 +201,11 @@ def wait_for_block(self, blockhash, timeout=60):
178201
self.sync(test_function, timeout)
179202
return
180203

204+
def wait_for_getheaders(self, timeout=60):
205+
test_function = lambda: self.last_getheaders != None
206+
self.sync(test_function, timeout)
207+
return
208+
181209
def wait_for_getdata(self, hash_list, timeout=60):
182210
if hash_list == []:
183211
return
@@ -186,6 +214,11 @@ def wait_for_getdata(self, hash_list, timeout=60):
186214
self.sync(test_function, timeout)
187215
return
188216

217+
def wait_for_disconnect(self, timeout=60):
218+
test_function = lambda: self.disconnected
219+
self.sync(test_function, timeout)
220+
return
221+
189222
def send_header_for_blocks(self, new_blocks):
190223
headers_message = msg_headers()
191224
headers_message.headers = [ CBlockHeader(b) for b in new_blocks ]
@@ -510,6 +543,78 @@ def run_test(self):
510543

511544
print("Part 4: success!")
512545

546+
# Now deliver all those blocks we announced.
547+
[ test_node.send_message(msg_block(x)) for x in blocks ]
548+
549+
print("Part 5: Testing handling of unconnecting headers")
550+
# First we test that receipt of an unconnecting header doesn't prevent
551+
# chain sync.
552+
for i in range(10):
553+
test_node.last_getdata = None
554+
blocks = []
555+
# Create two more blocks.
556+
for j in range(2):
557+
blocks.append(create_block(tip, create_coinbase(height), block_time))
558+
blocks[-1].solve()
559+
tip = blocks[-1].sha256
560+
block_time += 1
561+
height += 1
562+
# Send the header of the second block -> this won't connect.
563+
with mininode_lock:
564+
test_node.last_getheaders = None
565+
test_node.send_header_for_blocks([blocks[1]])
566+
test_node.wait_for_getheaders(timeout=1)
567+
test_node.send_header_for_blocks(blocks)
568+
test_node.wait_for_getdata([x.sha256 for x in blocks])
569+
[ test_node.send_message(msg_block(x)) for x in blocks ]
570+
test_node.sync_with_ping()
571+
assert_equal(int(self.nodes[0].getbestblockhash(), 16), blocks[1].sha256)
572+
573+
blocks = []
574+
# Now we test that if we repeatedly don't send connecting headers, we
575+
# don't go into an infinite loop trying to get them to connect.
576+
MAX_UNCONNECTING_HEADERS = 10
577+
for j in range(MAX_UNCONNECTING_HEADERS+1):
578+
blocks.append(create_block(tip, create_coinbase(height), block_time))
579+
blocks[-1].solve()
580+
tip = blocks[-1].sha256
581+
block_time += 1
582+
height += 1
583+
584+
for i in range(1, MAX_UNCONNECTING_HEADERS):
585+
# Send a header that doesn't connect, check that we get a getheaders.
586+
with mininode_lock:
587+
test_node.last_getheaders = None
588+
test_node.send_header_for_blocks([blocks[i]])
589+
test_node.wait_for_getheaders(timeout=1)
590+
591+
# Next header will connect, should re-set our count:
592+
test_node.send_header_for_blocks([blocks[0]])
593+
594+
# Remove the first two entries (blocks[1] would connect):
595+
blocks = blocks[2:]
596+
597+
# Now try to see how many unconnecting headers we can send
598+
# before we get disconnected. Should be 5*MAX_UNCONNECTING_HEADERS
599+
for i in range(5*MAX_UNCONNECTING_HEADERS - 1):
600+
# Send a header that doesn't connect, check that we get a getheaders.
601+
with mininode_lock:
602+
test_node.last_getheaders = None
603+
test_node.send_header_for_blocks([blocks[i%len(blocks)]])
604+
test_node.wait_for_getheaders(timeout=1)
605+
606+
# Eventually this stops working.
607+
with mininode_lock:
608+
self.last_getheaders = None
609+
test_node.send_header_for_blocks([blocks[-1]])
610+
611+
# Should get disconnected
612+
test_node.wait_for_disconnect()
613+
with mininode_lock:
614+
self.last_getheaders = True
615+
616+
print("Part 5: success!")
617+
513618
# Finally, check that the inv node never received a getdata request,
514619
# throughout the test
515620
assert_equal(inv_node.last_getdata, None)

src/main.cpp

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,8 @@ struct CNodeState {
276276
CBlockIndex *pindexLastCommonBlock;
277277
//! The best header we have sent our peer.
278278
CBlockIndex *pindexBestHeaderSent;
279+
//! Length of current-streak of unconnecting headers announcements
280+
int nUnconnectingHeaders;
279281
//! Whether we've started headers synchronization with this peer.
280282
bool fSyncStarted;
281283
//! Since when we're stalling block download progress (in microseconds), or 0.
@@ -304,6 +306,7 @@ struct CNodeState {
304306
hashLastUnknownBlock.SetNull();
305307
pindexLastCommonBlock = NULL;
306308
pindexBestHeaderSent = NULL;
309+
nUnconnectingHeaders = 0;
307310
fSyncStarted = false;
308311
nStallingSince = 0;
309312
nDownloadingSince = 0;
@@ -5773,6 +5776,35 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv,
57735776
return true;
57745777
}
57755778

5779+
CNodeState *nodestate = State(pfrom->GetId());
5780+
5781+
// If this looks like it could be a block announcement (nCount <
5782+
// MAX_BLOCKS_TO_ANNOUNCE), use special logic for handling headers that
5783+
// don't connect:
5784+
// - Send a getheaders message in response to try to connect the chain.
5785+
// - The peer can send up to MAX_UNCONNECTING_HEADERS in a row that
5786+
// don't connect before giving DoS points
5787+
// - Once a headers message is received that is valid and does connect,
5788+
// nUnconnectingHeaders gets reset back to 0.
5789+
if (mapBlockIndex.find(headers[0].hashPrevBlock) == mapBlockIndex.end() && nCount < MAX_BLOCKS_TO_ANNOUNCE) {
5790+
nodestate->nUnconnectingHeaders++;
5791+
pfrom->PushMessage(NetMsgType::GETHEADERS, chainActive.GetLocator(pindexBestHeader), uint256());
5792+
LogPrint("net", "received header %s: missing prev block %s, sending getheaders (%d) to end (peer=%d, nUnconnectingHeaders=%d)\n",
5793+
headers[0].GetHash().ToString(),
5794+
headers[0].hashPrevBlock.ToString(),
5795+
pindexBestHeader->nHeight,
5796+
pfrom->id, nodestate->nUnconnectingHeaders);
5797+
// Set hashLastUnknownBlock for this peer, so that if we
5798+
// eventually get the headers - even from a different peer -
5799+
// we can use this peer to download.
5800+
UpdateBlockAvailability(pfrom->GetId(), headers.back().GetHash());
5801+
5802+
if (nodestate->nUnconnectingHeaders % MAX_UNCONNECTING_HEADERS == 0) {
5803+
Misbehaving(pfrom->GetId(), 20);
5804+
}
5805+
return true;
5806+
}
5807+
57765808
CBlockIndex *pindexLast = NULL;
57775809
BOOST_FOREACH(const CBlockHeader& header, headers) {
57785810
CValidationState state;
@@ -5790,6 +5822,11 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv,
57905822
}
57915823
}
57925824

5825+
if (nodestate->nUnconnectingHeaders > 0) {
5826+
LogPrint("net", "peer=%d: resetting nUnconnectingHeaders (%d -> 0)\n", pfrom->id, nodestate->nUnconnectingHeaders);
5827+
}
5828+
nodestate->nUnconnectingHeaders = 0;
5829+
57935830
assert(pindexLast);
57945831
UpdateBlockAvailability(pfrom->GetId(), pindexLast->GetBlockHash());
57955832

@@ -5802,7 +5839,6 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv,
58025839
}
58035840

58045841
bool fCanDirectFetch = CanDirectFetch(chainparams.GetConsensus());
5805-
CNodeState *nodestate = State(pfrom->GetId());
58065842
// If this set of headers is valid and ends in a block with at least as
58075843
// much work as our tip, download as much as possible.
58085844
if (fCanDirectFetch && pindexLast->IsValid(BLOCK_VALID_TREE) && chainActive.Tip()->nChainWork <= pindexLast->nChainWork) {

src/main.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,9 @@ static const bool DEFAULT_FEEFILTER = true;
138138
/** Maximum number of headers to announce when relaying blocks with headers message.*/
139139
static const unsigned int MAX_BLOCKS_TO_ANNOUNCE = 8;
140140

141+
/** Maximum number of unconnecting headers announcements before DoS score */
142+
static const int MAX_UNCONNECTING_HEADERS = 10;
143+
141144
static const bool DEFAULT_PEERBLOOMFILTERS = true;
142145

143146
struct BlockHasher

0 commit comments

Comments
 (0)