Skip to content

Commit 0a072b2

Browse files
Merge bitcoin#33296: net: check for empty header before calling FillBlock
Backport of bitcoin#33296 (commits 5e585a0, 8b62647), slimmed for Dash. Guards ProcessCompactBlockTxns against re-entering PartiallyDownloadedBlock::FillBlock when a prior reconstruction attempt returned READ_STATUS_FAILED and left the (now header-less) partial block in flight; such a second blocktxn now discourages and disconnects the peer with a clear reason. Adds the test_multiple_blocktxn_response regression test to p2p_compactblocks.py. Dash adaptations: no LookupBlockIndex/DEPLOYMENT_SEGWIT (Dash's FillBlock takes no segwit_active parameter), and Misbehaving(pfrom.GetId(), 100, ...). Note on the double-blocktxn SIGABRT: on this branch that crash is already eliminated by bitcoin#26898 (FillBlock returns READ_STATUS_INVALID instead of asserting on a null header, which the caller then handles by discouraging and removing the block request). Upstream's guard exists to avoid a LookupBlockIndex/Assume() on the wiped header, which Dash does not perform. This guard is therefore a faithful belt-and-suspenders port -- it rejects the duplicate earlier with a clearer message and skips a wasted reconstruction pass -- rather than the fix for a still-reachable crash.
1 parent f22ed84 commit 0a072b2

2 files changed

Lines changed: 55 additions & 0 deletions

File tree

src/net_processing.cpp

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3657,6 +3657,17 @@ void PeerManagerImpl::ProcessCompactBlockTxns(CNode& pfrom, Peer& peer, const Bl
36573657
}
36583658

36593659
PartiallyDownloadedBlock& partialBlock = *range_flight.first->second.second->partialBlock;
3660+
3661+
if (partialBlock.header.IsNull()) {
3662+
// It is possible for the header to be empty if a previous call to FillBlock wiped the header but
3663+
// left the PartiallyDownloadedBlock pointer around (i.e. a prior reconstruction attempt returned
3664+
// READ_STATUS_FAILED without calling RemoveBlockRequest). Don't attempt to reconstruct again.
3665+
RemoveBlockRequest(block_transactions.blockhash, pfrom.GetId());
3666+
Misbehaving(peer, 100, "previous compact block reconstruction attempt failed");
3667+
LogPrint(BCLog::NET, "Peer %d sent compact block transactions multiple times\n", pfrom.GetId());
3668+
return;
3669+
}
3670+
36603671
ReadStatus status = partialBlock.FillBlock(*pblock, block_transactions.txn);
36613672
if (status == READ_STATUS_INVALID) {
36623673
RemoveBlockRequest(block_transactions.blockhash, pfrom.GetId()); // Reset in-flight state in case Misbehaving does not result in a disconnect

test/functional/p2p_compactblocks.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -556,6 +556,45 @@ def test_incorrect_blocktxn_response(self, test_node):
556556
test_node.send_and_ping(msg_block(block))
557557
assert_equal(int(node.getbestblockhash(), 16), block.sha256)
558558

559+
# Multiple blocktxn responses for the same in-flight block must not cause a
560+
# re-entrant FillBlock (which previously hit an assert); the second response
561+
# should instead get the peer disconnected (see bitcoin#33296).
562+
def test_multiple_blocktxn_response(self, test_node):
563+
node = self.nodes[0]
564+
utxo = self.utxos[0]
565+
566+
block = self.build_block_with_transactions(node, utxo, 2)
567+
568+
# Send compact block, prefilling only the coinbase so the node has to
569+
# request the other two transactions via getblocktxn.
570+
comp_block = HeaderAndShortIDs()
571+
comp_block.initialize_from_block(block, prefill_list=[0])
572+
test_node.send_and_ping(msg_cmpctblock(comp_block.to_p2p()))
573+
with p2p_lock:
574+
assert "getblocktxn" in test_node.last_message
575+
absolute_indexes = test_node.last_message["getblocktxn"].block_txn_request.to_absolute()
576+
assert_equal(absolute_indexes, [1, 2])
577+
578+
# Respond with the transactions in the wrong order so reconstruction
579+
# fails (merkle root mismatch), triggering the getdata fallback while
580+
# the PartiallyDownloadedBlock is left in flight.
581+
msg = msg_blocktxn()
582+
msg.block_transactions = BlockTransactions(block.sha256, [block.vtx[2]] + [block.vtx[1]])
583+
test_node.send_and_ping(msg)
584+
585+
# Tip should not have updated
586+
assert_equal(int(node.getbestblockhash(), 16), block.hashPrevBlock)
587+
588+
# We should receive a getdata request for the full block
589+
test_node.wait_for_getdata([block.sha256], timeout=10)
590+
assert test_node.last_message["getdata"].inv[0].type == MSG_BLOCK
591+
592+
# Sending the same blocktxn again must get the peer disconnected rather
593+
# than re-entering FillBlock on the (now header-less) partial block.
594+
with node.assert_debug_log(['previous compact block reconstruction attempt failed']):
595+
test_node.send_message(msg)
596+
test_node.wait_for_disconnect()
597+
559598
def test_getblocktxn_handler(self, test_node):
560599
node = self.nodes[0]
561600
# dashd will not send blocktxn responses for blocks whose height is
@@ -921,5 +960,10 @@ def run_test(self):
921960
self.log.info("Testing high-bandwidth mode states via getpeerinfo...")
922961
self.test_highbandwidth_mode_states_via_getpeerinfo()
923962

963+
self.log.info("Testing handling of multiple blocktxn responses...")
964+
# Earlier tests may have left self.test_node disconnected; use a fresh peer.
965+
self.test_node = self.nodes[0].add_p2p_connection(TestP2PConn())
966+
self.test_multiple_blocktxn_response(self.test_node)
967+
924968
if __name__ == '__main__':
925969
CompactBlocksTest().main()

0 commit comments

Comments
 (0)