Inside Bitcoin
Opening the selected file…
Inside Bitcoin
Opening the selected file…
The code behind the network, with room to understand it.
Release published 2026-10-28 · Snapshot fetched 2026-11-01. Latest stable release at fetch time; development branches and Git history are excluded. Only the selected code window and visible folders load here.
1#!/usr/bin/env python32# Copyright (c) 2019-present The Bitcoin Core developers3# Distributed under the MIT software license, see the accompanying4# file COPYING or http://www.opensource.org/licenses/mit-license.php.6""" Test node outbound peer eviction logic8A subset of our outbound peers are subject to eviction logic if they cannot keep up9with our vision of the best chain. This criteria applies only to non-protected peers,10and can be triggered by either not learning about any blocks from an outbound peer after11a certain deadline, or by them not being able to catch up fast enough (under the same deadline).13This tests the different eviction paths based on the peer's behavior and on whether they are protected14or not.15"""16import time18from test_framework.messages import (19from_hex,20msg_headers,21CBlockHeader,22)23from test_framework.p2p import P2PInterface24from test_framework.test_framework import BitcoinTestFramework26# Timeouts (in seconds)27CHAIN_SYNC_TIMEOUT = 20 * 6028HEADERS_RESPONSE_TIME = 2 * 6031class P2POutEvict(BitcoinTestFramework):32def set_test_params(self):33self.num_nodes = 135def test_outbound_eviction_unprotected(self):36# This tests the eviction logic for **unprotected** outbound peers (that is, PeerManagerImpl::ConsiderEviction)37node = self.nodes[0]38cur_mock_time = node.mocktime40# Get our tip header and its parent41tip_header = from_hex(CBlockHeader(), node.getblockheader(node.getbestblockhash(), False))42prev_header = from_hex(CBlockHeader(), node.getblockheader(f"{tip_header.hashPrevBlock:064x}", False))44self.log.info("Create an outbound connection and don't send any headers")45# Test disconnect due to no block being announced in 22+ minutes (headers are not even exchanged)46peer = node.add_outbound_p2p_connection(P2PInterface(), p2p_idx=0, connection_type="outbound-full-relay")47# Wait for over 20 min to trigger the first eviction timeout. This sets the last call past 2 min in the future.48cur_mock_time += (CHAIN_SYNC_TIMEOUT + 1)49node.setmocktime(cur_mock_time)50peer.sync_with_ping()51# Wait for over 2 more min to trigger the disconnection52peer.wait_for_getheaders(block_hash=tip_header.hashPrevBlock)53cur_mock_time += (HEADERS_RESPONSE_TIME + 1)54node.setmocktime(cur_mock_time)55self.log.info("Test that the peer gets evicted")56peer.wait_for_disconnect()58self.log.info("Create an outbound connection and send header but the peer never catches up")59# Mimic a node that just falls behind for long enough60# This should also apply for a node doing IBD that does not catch up in time61# Connect a peer and make it send us headers ending in our tip's parent62peer = node.add_outbound_p2p_connection(P2PInterface(), p2p_idx=0, connection_type="outbound-full-relay")63peer.send_and_ping(msg_headers([prev_header]))65# Trigger the timeouts66cur_mock_time += (CHAIN_SYNC_TIMEOUT + 1)67node.setmocktime(cur_mock_time)68peer.sync_with_ping()69peer.wait_for_getheaders(block_hash=tip_header.hashPrevBlock)70cur_mock_time += (HEADERS_RESPONSE_TIME + 1)71node.setmocktime(cur_mock_time)72self.log.info("Test that the peer gets evicted")73peer.wait_for_disconnect()75self.log.info("Create an outbound connection and keep lagging behind, but not too much")76# Test that if the peer never catches up with our current tip, but it does with the77# expected work that we set when setting the timer (that is, our tip at the time)78# the node does not disconnect the peer79peer = node.add_outbound_p2p_connection(P2PInterface(), p2p_idx=0, connection_type="outbound-full-relay")81self.log.info("Mine a block so our peer starts lagging")82prev_prev_hash = tip_header.hashPrevBlock83best_block_hash = self.generateblock(node, output="raw(42)", transactions=[])["hash"]84peer.sync_with_ping()86self.log.info("The peer keeps catching up with the old tip; check that the node does not evict the peer")87for i in range(10):88# Generate an additional block so the peers is 2 blocks behind89prev_header = from_hex(CBlockHeader(), node.getblockheader(best_block_hash, False))90best_block_hash = self.generateblock(node, output="raw(42)", transactions=[])["hash"]91tip_header = from_hex(CBlockHeader(), node.getblockheader(best_block_hash, False))92peer.sync_with_ping()94# Advance time but not enough to evict the peer95cur_mock_time += (CHAIN_SYNC_TIMEOUT + 1)96node.setmocktime(cur_mock_time)97peer.sync_with_ping()99# Make the peer wait until it gets node's last call (by receiving a getheaders)100peer.wait_for_getheaders(block_hash=prev_prev_hash)102# The peer sends a header with the previous tip (so the peer goes back to 1 block behind)103peer.send_and_ping(msg_headers([prev_header]))104prev_prev_hash = tip_header.hashPrevBlock106self.log.info("Create an outbound connection and take some time to catch up, but do it in time")107# Check that if the peer manages to catch up within time, the timeouts are removed (and the peer is not disconnected)108# We are reusing the peer from the previous case which already sent the node a valid (but old) block and whose timer is ticking110# Make the peer send an updated headers message matching our tip111peer.send_and_ping(msg_headers([from_hex(CBlockHeader(), node.getblockheader(best_block_hash, False))]))113# Wait for long enough for the timeouts to have triggered and check that the peer is still connected114cur_mock_time += (CHAIN_SYNC_TIMEOUT + 1)115node.setmocktime(cur_mock_time)116peer.sync_with_ping()117cur_mock_time += (HEADERS_RESPONSE_TIME + 1)118node.setmocktime(cur_mock_time)119self.log.info("Test that the peer does not get evicted")120peer.sync_with_ping()122node.disconnect_p2ps()124def test_outbound_eviction_protected(self):125# This tests the eviction logic for **protected** outbound peers (that is, PeerManagerImpl::ConsiderEviction)126# Outbound connections are flagged as protected if:127# - The peer sends a connecting block with at least as much work as our current tip.128# - There are still available slots in the node's protected_peers list.129# This test ensures that such protected outbound peers are not disconnected even after chain sync and headers timeouts.130node = self.nodes[0]131cur_mock_time = node.mocktime132tip_header = from_hex(CBlockHeader(), node.getblockheader(node.getbestblockhash(), False))134self.log.info("Create an outbound connection to a peer that shares our tip so it gets granted protection")135peer = node.add_outbound_p2p_connection(P2PInterface(), p2p_idx=0, connection_type="outbound-full-relay")136peer.send_and_ping(msg_headers([tip_header]))138self.log.info("Mine a new block and sync with our peer")139self.generateblock(node, output="raw(42)", transactions=[])140peer.sync_with_ping()142self.log.info("Let enough time pass for the timeouts to go off")143# Trigger the timeouts and check how we are still connected144cur_mock_time += (CHAIN_SYNC_TIMEOUT + 1)145node.setmocktime(cur_mock_time)146peer.sync_with_ping()147peer.wait_for_getheaders(block_hash=tip_header.hashPrevBlock)148cur_mock_time += (HEADERS_RESPONSE_TIME + 1)149node.setmocktime(cur_mock_time)150self.log.info("Test that the peer does not get evicted")151peer.sync_with_ping()153node.disconnect_p2ps()155def test_outbound_eviction_mixed(self):156# This tests the outbound eviction logic for a mix of protected and unprotected peers.157node = self.nodes[0]158cur_mock_time = node.mocktime160self.log.info("Create a mix of protected and unprotected outbound connections to check against eviction")162# Let's try this logic having multiple peers, some protected and some unprotected163# We protect up to 4 peers as long as they have provided a block with the same amount of work as our tip164self.log.info("The first 4 peers are protected by sending us a valid block with enough work")165tip_header = from_hex(CBlockHeader(), node.getblockheader(node.getbestblockhash(), False))166headers_message = msg_headers([tip_header])167protected_peers = []168for i in range(4):169peer = node.add_outbound_p2p_connection(P2PInterface(), p2p_idx=i, connection_type="outbound-full-relay")170peer.send_and_ping(headers_message)171protected_peers.append(peer)173# We can create 4 additional outbound connections to peers that are unprotected. 2 of them will be well behaved,174# whereas the other 2 will misbehave (1 sending no headers, 1 sending old ones)175self.log.info("The remaining 4 peers will be mixed between honest (2) and misbehaving peers (2)")176prev_header = from_hex(CBlockHeader(), node.getblockheader(f"{tip_header.hashPrevBlock:064x}", False))177headers_message = msg_headers([prev_header])178honest_unprotected_peers = []179for i in range(2):180peer = node.add_outbound_p2p_connection(P2PInterface(), p2p_idx=4+i, connection_type="outbound-full-relay")181peer.send_and_ping(headers_message)182honest_unprotected_peers.append(peer)184misbehaving_unprotected_peers = []185for i in range(2):186peer = node.add_outbound_p2p_connection(P2PInterface(), p2p_idx=6+i, connection_type="outbound-full-relay")187if i%2==0:188peer.send_and_ping(headers_message)189misbehaving_unprotected_peers.append(peer)191self.log.info("Mine a new block and keep the unprotected honest peer on sync, all the rest off-sync")192# Mine a block so all peers become outdated193target_hash = prev_header.hash_int194tip_hash = self.generateblock(node, output="raw(42)", transactions=[])["hash"]195tip_header = from_hex(CBlockHeader(), node.getblockheader(tip_hash, False))196tip_headers_message = msg_headers([tip_header])198# Let the timeouts hit and check back199cur_mock_time += (CHAIN_SYNC_TIMEOUT + 1)200node.setmocktime(cur_mock_time)201for peer in protected_peers + misbehaving_unprotected_peers:202peer.sync_with_ping()203peer.wait_for_getheaders(block_hash=target_hash)204for peer in honest_unprotected_peers:205peer.send_and_ping(tip_headers_message)206peer.wait_for_getheaders(block_hash=target_hash)208cur_mock_time += (HEADERS_RESPONSE_TIME + 1)209node.setmocktime(cur_mock_time)210self.log.info("Check that none of the honest or protected peers were evicted, but all misbehaving unprotected peers were")211for peer in protected_peers + honest_unprotected_peers:212peer.sync_with_ping()213for peer in misbehaving_unprotected_peers:214peer.wait_for_disconnect()216node.disconnect_p2ps()218def test_outbound_eviction_blocks_relay_only(self):219# The logic for outbound eviction protection only applies to outbound-full-relay peers220# This tests that other types of peers (blocks-relay-only for instance) are not granted protection221node = self.nodes[0]222cur_mock_time = node.mocktime223tip_header = from_hex(CBlockHeader(), node.getblockheader(node.getbestblockhash(), False))225self.log.info("Create an blocks-only outbound connection to a peer that shares our tip. This would usually grant protection")226peer = node.add_outbound_p2p_connection(P2PInterface(), p2p_idx=0, connection_type="block-relay-only")227peer.send_and_ping(msg_headers([tip_header]))229self.log.info("Mine a new block and sync with our peer")230self.generateblock(node, output="raw(42)", transactions=[])231peer.sync_with_ping()233self.log.info("Let enough time pass for the timeouts to go off")234# Trigger the timeouts and check how the peer gets evicted, since protection is only given to outbound-full-relay peers235cur_mock_time += (CHAIN_SYNC_TIMEOUT + 1)236node.setmocktime(cur_mock_time)237peer.sync_with_ping()238peer.wait_for_getheaders(block_hash=tip_header.hash_int)239cur_mock_time += (HEADERS_RESPONSE_TIME + 1)240node.setmocktime(cur_mock_time)241self.log.info("Test that the peer gets evicted")242peer.wait_for_disconnect()244node.disconnect_p2ps()247def run_test(self):248self.nodes[0].setmocktime(int(time.time()))249self.test_outbound_eviction_unprotected()250self.test_outbound_eviction_protected()
Independent explanations by MyBitcoinExplorer. Not affiliated with or endorsed by Bitcoin Core. Guides are educational and do not replace the source or its tests.
Bitcoin Core source © its contributors, distributed under the MIT license. Original notices and bundled third-party licenses are retained in the source tree.